/******************************************************************************* * Copyright (c) 2010, 2020 Michael Clay and others. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0. * * SPDX-License-Identifier: EPL-2.0 * *******************************************************************************/ package org.eclipse.xtext.xtext.ui.editor.quickfix; import static org.eclipse.xtext.xtext.XtextConfigurableIssueCodes.*; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.util.AbstractList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.emf.codegen.ecore.genmodel.GenModel; import org.eclipse.emf.codegen.ecore.genmodel.GenModelPackage; import org.eclipse.emf.codegen.ecore.genmodel.GenPackage; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EClassifier; import org.eclipse.emf.ecore.EDataType; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EPackage; import org.eclipse.emf.ecore.EStructuralFeature.Setting; import org.eclipse.emf.ecore.EcorePackage; import org.eclipse.emf.ecore.plugin.EcorePlugin; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.emf.ecore.xmi.XMLResource; import org.eclipse.emf.ecore.xmi.impl.URIHandlerImpl; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.text.BadLocationException; import org.eclipse.swt.widgets.Shell; import org.eclipse.text.edits.InsertEdit; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.actions.WorkspaceModifyOperation; import org.eclipse.xtext.AbstractElement; import org.eclipse.xtext.AbstractMetamodelDeclaration; import org.eclipse.xtext.AbstractRule; import org.eclipse.xtext.Alternatives; import org.eclipse.xtext.EcoreUtil2; import org.eclipse.xtext.EnumLiteralDeclaration; import org.eclipse.xtext.GrammarUtil; import org.eclipse.xtext.Keyword; import org.eclipse.xtext.ParserRule; import org.eclipse.xtext.RuleCall; import org.eclipse.xtext.TerminalRule; import org.eclipse.xtext.TypeRef; import org.eclipse.xtext.XtextFactory; import org.eclipse.xtext.XtextPackage; import org.eclipse.xtext.conversion.IValueConverterService; import org.eclipse.xtext.formatting.ILineSeparatorInformation; import org.eclipse.xtext.naming.QualifiedName; import org.eclipse.xtext.nodemodel.ICompositeNode; import org.eclipse.xtext.nodemodel.INode; import org.eclipse.xtext.nodemodel.util.NodeModelUtils; import org.eclipse.xtext.resource.IEObjectDescription; import org.eclipse.xtext.resource.IResourceDescriptions; import org.eclipse.xtext.resource.XtextResource; import org.eclipse.xtext.resource.impl.ResourceDescriptionsProvider; import org.eclipse.xtext.ui.editor.model.IXtextDocument; import org.eclipse.xtext.ui.editor.model.edit.IModification; import org.eclipse.xtext.ui.editor.model.edit.IModificationContext; import org.eclipse.xtext.ui.editor.model.edit.ISemanticModification; import org.eclipse.xtext.ui.editor.quickfix.DefaultQuickfixProvider; import org.eclipse.xtext.ui.editor.quickfix.Fix; import org.eclipse.xtext.ui.editor.quickfix.IssueResolutionAcceptor; import org.eclipse.xtext.util.ITextRegion; import org.eclipse.xtext.util.Strings; import org.eclipse.xtext.util.concurrent.IUnitOfWork; import org.eclipse.xtext.util.internal.Nullable; import org.eclipse.xtext.validation.Issue; import org.eclipse.xtext.xtext.RuleWithoutInstantiationInspector; import org.eclipse.xtext.xtext.XtextLinkingDiagnosticMessageProvider; import com.google.common.base.CaseFormat; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.inject.Inject; /** * @author Michael Clay - Initial contribution and API * @author Sebastian Zarnekow - Quickfixes for bogus EPackage imports */ public class XtextGrammarQuickfixProvider extends DefaultQuickfixProvider { private static final String GRAMMAR_LANG_DOC = "https://www.eclipse.org/Xtext/documentation/301_grammarlanguage.html"; private String NULL_QUICKFIX_IMAGE = null; // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=324566 // // @Fix(XtextValidator.INVALID_METAMODEL_ALIAS) // public void fixInvalidMetaModelAlias(final Issue issue, IssueResolutionAcceptor acceptor) { // final String alias = issue.getData()[0]; // acceptor.accept(issue, "Remove '" + alias + "' alias", "Remove '" + alias + "' alias", NULL_QUICKFIX_IMAGE, // new ISemanticModification() { // public void apply(final EObject element, IModificationContext context) { // ReferencedMetamodel referencedMetamodel = (ReferencedMetamodel) element; // referencedMetamodel.setAlias(null); // } // }); // createLinkingIssueResolutions(issue, acceptor); // } @Inject private IValueConverterService valueConverterService; @Inject private ResourceDescriptionsProvider resourceDescriptionsProvider; @Inject(optional=true) @Nullable private IWorkbench workbench; @Inject private ILineSeparatorInformation separatorInfo; @Fix(XtextLinkingDiagnosticMessageProvider.UNRESOLVED_RULE) public void fixUnresolvedRule(final Issue issue, IssueResolutionAcceptor acceptor) { final String ruleName = issue.getData()[0]; acceptor.accept(issue, "Create rule '" + ruleName + "'", "Create rule '" + ruleName + "'", NULL_QUICKFIX_IMAGE, new ISemanticModification() { @Override public void apply(final EObject element, IModificationContext context) throws BadLocationException { AbstractRule abstractRule = EcoreUtil2.getContainerOfType(element, ParserRule.class); ICompositeNode node = NodeModelUtils.getNode(abstractRule); int offset = node.getEndOffset(); String nl = context.getXtextDocument().getLineDelimiter(0); StringBuilder builder = new StringBuilder(nl+nl); if (abstractRule instanceof TerminalRule) builder.append("terminal "); String newRule = builder.append(ruleName).append(":" + nl + "\t" + nl + ";").toString(); context.getXtextDocument().replace(offset, 0, newRule); } }); createLinkingIssueResolutions(issue, acceptor); } @Fix(INVALID_METAMODEL_NAME) public void fixInvalidMetaModelName(final Issue issue, IssueResolutionAcceptor acceptor) { final String metaModelName = issue.getData()[0]; acceptor.accept(issue, "Fix metamodel name '" + metaModelName + "'", "Fix metamodel name '" + metaModelName + "'", NULL_QUICKFIX_IMAGE, new IModification() { @Override public void apply(IModificationContext context) throws Exception { context.getXtextDocument().replace(issue.getOffset(), issue.getLength(), Strings.toFirstLower(metaModelName)); } }); } @Fix(EMPTY_ENUM_LITERAL) public void fixEmptyEnumLiteral(final Issue issue, IssueResolutionAcceptor acceptor) { acceptor.acceptMulti(issue, "Fix empty enum literal", "Fix empty enum literal", NULL_QUICKFIX_IMAGE, (EObject element) -> { EnumLiteralDeclaration enumLiteralDeclaration = (EnumLiteralDeclaration) element; Keyword keyword = XtextFactory.eINSTANCE.createKeyword(); keyword.setValue(enumLiteralDeclaration.getEnumLiteral().getName().toLowerCase()); enumLiteralDeclaration.setLiteral(keyword); }); } @Fix(EMPTY_KEYWORD) public void removeEmptyKeyword(final Issue issue, IssueResolutionAcceptor acceptor) { acceptor.accept(issue, "Remove empty keyword", "Remove empty keyword", NULL_QUICKFIX_IMAGE, new IModification() { @Override public void apply(IModificationContext context) throws Exception { context.getXtextDocument().replace(issue.getOffset(), issue.getLength(), ""); } }); } @Fix(EMPTY_KEYWORD) public void replaceEmptyKeywordWithRuleName(final Issue issue, IssueResolutionAcceptor acceptor) { acceptor.accept(issue, "Replace empty keyword with rule name", "Replace empty keyword with rule name", NULL_QUICKFIX_IMAGE, new IModification() { @Override public void apply(IModificationContext context) throws Exception { final IXtextDocument document = context.getXtextDocument(); final String containingRuleName = document.tryPriorityReadOnly(new IUnitOfWork<String, XtextResource>() { @Override public String exec(XtextResource state) throws Exception { return Optional.ofNullable(issue.getUriToProblem().fragment()).map(state::getEObject) .map(GrammarUtil::containingRule).map(AbstractRule::getName).map(Strings::toFirstLower) .orElse(null); } }); if (containingRuleName != null) { final String quote = String.valueOf(document.getChar(issue.getOffset())); document.replace(issue.getOffset(), issue.getLength(), quote + containingRuleName + quote); } } }); } @Fix(SPACES_IN_KEYWORD) public void fixKeywordNoSpaces(final Issue issue, IssueResolutionAcceptor acceptor) { acceptor.accept(issue, "Fix keyword with spaces", "Fix keyword with spaces", NULL_QUICKFIX_IMAGE, new IModification() { @Override public void apply(IModificationContext context) throws Exception { final int offset = issue.getOffset(); final int length = issue.getLength(); final IXtextDocument document = context.getXtextDocument(); final String quote = String.valueOf(document.getChar(offset)); final String identifiers = document.get(offset, length).replaceAll("'|\"", "").trim(); if (!Strings.isEmpty(identifiers)) { document.replace(offset, length, quote + Joiner.on(quote + " " + quote).join(identifiers.split("\\s+")) + quote); } else { final String containingRuleName = document.tryPriorityReadOnly(new IUnitOfWork<String, XtextResource>() { @Override public String exec(XtextResource state) throws Exception { return Optional.ofNullable(issue.getUriToProblem().fragment()).map(state::getEObject) .map(GrammarUtil::containingRule).map(AbstractRule::getName).map(Strings::toFirstLower) .orElse(null); } }); if (containingRuleName != null) { document.replace(offset, length, quote + containingRuleName + quote); } } } }); } @Fix(INVALID_ACTION_USAGE) public void fixInvalidActionUsage(final Issue issue, IssueResolutionAcceptor acceptor) { acceptor.accept(issue, "Fix invalid action usage", "Fix invalid action usage", NULL_QUICKFIX_IMAGE, new IModification() { @Override public void apply(IModificationContext context) throws BadLocationException { context.getXtextDocument().replace(issue.getOffset(), issue.getLength(), ""); } }); } @Fix(INVALID_PACKAGE_REFERENCE_INHERITED) public void fixImportedPackageFromSuperGrammar(final Issue issue, IssueResolutionAcceptor acceptor) { if (issue.getData().length == 1) acceptor.accept(issue, "Change to '" + issue.getData()[0] + "'", "Fix the bogus package import\n" + "import '" + issue.getData()[0] + "'", NULL_QUICKFIX_IMAGE, new IModification() { @Override public void apply(IModificationContext context) throws BadLocationException { String replaceString = valueConverterService.toString(issue.getData()[0], "STRING"); IXtextDocument document = context.getXtextDocument(); String delimiter = document.get(issue.getOffset(), 1); if (!replaceString.startsWith(delimiter)) { replaceString = delimiter + replaceString.substring(1, replaceString.length() - 1) + delimiter; } document.replace(issue.getOffset(), issue.getLength(), replaceString); } }); } @Fix(INVALID_PACKAGE_REFERENCE_EXTERNAL) public void fixExternalImportedPackage(final Issue issue, IssueResolutionAcceptor acceptor) { if (issue.getData().length == 1) acceptor.accept(issue, "Update the imported package '" + issue.getData()[0] + "'", "Fix the bogus package import\n" + "import '" + issue.getData()[0] + "'", NULL_QUICKFIX_IMAGE, new IModification() { @Override public void apply(IModificationContext context) throws BadLocationException { String replaceString = valueConverterService.toString(issue.getData()[0], "STRING"); IXtextDocument document = context.getXtextDocument(); final List<String> importedPackages = document.priorityReadOnly(new IUnitOfWork<List<String>, XtextResource>() { @Override public List<String> exec(XtextResource state) throws Exception { IResourceDescriptions descriptions = resourceDescriptionsProvider.getResourceDescriptions(state); ResourceSet resourceSet = state.getResourceSet(); final Map<URI, URI> uriMap = Maps.newHashMap(); EPackage ePackage = loadPackageFromIndex(descriptions, resourceSet, uriMap, issue.getData()[0]); if (ePackage != null) { final Map<String, EPackage> packagePerNsURI = Maps.newHashMap(); packagePerNsURI.put(ePackage.getNsURI(), ePackage); final Set<URI> updatedReferences = fixReferencesInPackages(ePackage, packagePerNsURI, uriMap, descriptions, resourceSet); if (updatedReferences.isEmpty()) return null; Iterator<EPackage> iterator = packagePerNsURI.values().iterator(); while(iterator.hasNext()) { EPackage pack = iterator.next(); Resource resource = pack.eResource(); if (!resource.getURI().isPlatformResource()) { iterator.remove(); } } final List<String> result = Lists.newArrayList(); new WorkspaceModifyOperation( /* workspace lock */ ) { @Override protected void execute(IProgressMonitor monitor) throws CoreException, InvocationTargetException, InterruptedException { try { for(EPackage pack: packagePerNsURI.values()) { Resource resource = pack.eResource(); resource.save(Collections.singletonMap( XMLResource.OPTION_URI_HANDLER, new URIHandlerImpl.PlatformSchemeAware() { @Override public URI deresolve(URI uri) { // replace archive uris with platform:/plugin if (!uri.isArchive() || !updatedReferences.contains(uri)) { return super.deresolve(uri); } URI withoutFragment = uri.trimFragment(); if (uriMap.containsKey(withoutFragment)) { withoutFragment = uriMap.get(withoutFragment); } return super.deresolve(withoutFragment.appendFragment(uri.fragment())); } })); result.add(resource.getURI().toString()); } } catch(IOException ioe) { throw new InvocationTargetException(ioe); } } }.run(new NullProgressMonitor()); for(int i = resourceSet.getResources().size() - 1; i >= 0; i-- ) { Resource resource = resourceSet.getResources().get(i); if (!resource.getContents().isEmpty() && resource.getContents().get(0) instanceof GenModel) { resourceSet.getResources().remove(i); } } return result; } return null; } private Set<URI> fixReferencesInPackages(EPackage ePackage, Map<String, EPackage> packagePerNsURI, Map<URI, URI> uriMap, IResourceDescriptions descriptions, ResourceSet resourceSet) { Set<URI> result = Sets.newHashSet(); Map<EObject, Collection<Setting>> allReferences = EcoreUtil.CrossReferencer.find(Collections.singletonList(ePackage)); for(final Setting setting: Iterables.concat(allReferences.values())) { if (setting.getEStructuralFeature().isChangeable()) { final Object referenced = setting.get(true); List<Object> references = null; if (referenced instanceof EObject) { references = new AbstractList<Object>() { @Override public Object set(int index, Object element) { setting.set(element); return referenced; } @Override public Object get(int index) { return referenced; } @Override public int size() { return 1; } }; } else { @SuppressWarnings("unchecked") List<Object> casted = (List<Object>) referenced; references = casted; } for(int i = 0; i < references.size(); i++) { if (references.get(i) instanceof EObject) { EObject referencedEObject = (EObject) references.get(i); EPackage transitive = EcoreUtil2.getContainerOfType(referencedEObject, EPackage.class); if (isRegisteredPackage(transitive)) { if (referencedEObject instanceof EDataType) continue; if (referencedEObject == EcorePackage.Literals.EOBJECT) continue; EPackage fromWorkspace = packagePerNsURI.get(transitive.getNsURI()); if (fromWorkspace == null && !packagePerNsURI.containsKey(transitive.getNsURI())) { fromWorkspace = loadPackageFromIndex(descriptions, resourceSet, uriMap, transitive.getNsURI()); packagePerNsURI.put(transitive.getNsURI(), fromWorkspace); } if (fromWorkspace != null) { String fragment = transitive.eResource().getURIFragment(referencedEObject); EObject replacement = fromWorkspace.eResource().getEObject(fragment); if (replacement != null) { result.add(EcoreUtil.getURI(replacement)); references.set(i, replacement); } } } } } } } return result; } private boolean isRegisteredPackage(EPackage ePackage) { return ePackage != null && (ePackage.eResource() == null || ePackage.getNsURI().equals(ePackage.eResource().getURI().toString())); } private EPackage loadPackageFromIndex( IResourceDescriptions descriptions, ResourceSet resourceSet, Map<URI, URI> uriMap, String nsURI) { Iterable<IEObjectDescription> fixUs = descriptions.getExportedObjects( EcorePackage.Literals.EPACKAGE, QualifiedName.create(nsURI), false); for(IEObjectDescription description: fixUs) { if (description.getEObjectURI().isPlatformResource()) { EObject result = resourceSet.getEObject(description.getEObjectURI(), true); if (result instanceof EPackage) { return (EPackage) result; } } } URI genModelURI = EcorePlugin.getEPackageNsURIToGenModelLocationMap(false).get(nsURI); if (genModelURI != null) { Resource genmodelResource = resourceSet.getResource(genModelURI, true); GenModel genModel = (GenModel) genmodelResource.getContents().get(0); for(GenPackage genPackage: genModel.getGenPackages()) { Object object = genPackage.eGet(GenModelPackage.Literals.GEN_PACKAGE__ECORE_PACKAGE, false); if (object instanceof EObject) { EObject proxy = (EObject) object; URI proxyURI = EcoreUtil.getURI(proxy); URI resolvedProxyURI = proxyURI.resolve(genModelURI); if (nsURI.equals(genPackage.getEcorePackage().getNsURI())) { EPackage result = genPackage.getEcorePackage(); uriMap.put(result.eResource().getURI(), resolvedProxyURI.trimFragment()); return result; } } } } return null; } }); String delimiter = document.get(issue.getOffset(), 1); if (!replaceString.startsWith(delimiter)) { replaceString = delimiter + replaceString.substring(1, replaceString.length() - 1) + delimiter; } document.replace(issue.getOffset(), issue.getLength(), replaceString); if (importedPackages != null && !importedPackages.isEmpty()) { final Shell shell = workbench.getActiveWorkbenchWindow().getShell(); shell.getDisplay().asyncExec(new Runnable() { @Override public void run() { String title = "Please update the Ecore2XtextDslProjectContributor that generates the language."; String message = "Please make sure that the Ecore2XtextDslProjectContributor that generates the language is up-to date.\n" + "Especially important is the registration of the referenced packages.\n" + "Please refer to the reference documentation for details."; MessageDialog dialog = new MessageDialog(shell, title, null, message, MessageDialog.INFORMATION, new String[] { "Open Documentation", "Close" }, 1); if (dialog.open() == 0) { try { workbench.getBrowserSupport().getExternalBrowser().openURL(new URL(GRAMMAR_LANG_DOC)); } catch (Exception e) { // ignore } } }}); } } }); } @Fix(INVALID_TERMINALRULE_NAME) public void fixTerminalRuleName(final Issue issue, IssueResolutionAcceptor acceptor){ if(issue.getData().length == 1){ final String upperCase = CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE,issue.getData()[0]).toString(); acceptor.accept(issue, "Change name to " + upperCase , "Change name to " + upperCase, "upcase.png", new IModification() { @Override public void apply(IModificationContext context) throws Exception { final IXtextDocument xtextDocument = context.getXtextDocument(); xtextDocument.replace(issue.getOffset(), issue.getLength(), upperCase); xtextDocument.modify(new IUnitOfWork.Void<XtextResource>() { @Override public void process(XtextResource state) throws Exception { final EObject terminalRule = state.getEObject(issue.getUriToProblem().fragment()); Iterable<RuleCall> candidates = Iterables.filter(Iterables.filter(Lists.newArrayList(state.getAllContents()),RuleCall.class), new Predicate<RuleCall>() { @Override public boolean apply(RuleCall ruleCall) { return ruleCall.getRule() == terminalRule; } }); for(RuleCall ruleCall: candidates){ List<INode> nodes = NodeModelUtils.findNodesForFeature(ruleCall, XtextPackage.eINSTANCE.getRuleCall_Rule()); for(INode node : nodes){ ITextRegion textRegion = node.getTextRegion(); xtextDocument.replace(textRegion.getOffset(), textRegion.getLength(), upperCase); } } } }); } }); } } @Fix(EXPLICIT_OVERRIDE_MISSING) public void addOverrideTag(final Issue issue, IssueResolutionAcceptor acceptor) { acceptor.accept(issue, "Add missing @Override annotation", "Inserts the missing @Override annotation.", null, new IModification() { @Override public void apply(IModificationContext context) throws Exception { final URI uri= issue.getUriToProblem(); final IXtextDocument document = context.getXtextDocument(uri); if (document == null) { return; } final Integer offset = document.tryReadOnly(new IUnitOfWork<Integer, XtextResource>() { @Override public Integer exec(XtextResource state) throws Exception { final EObject eObject = state.getEObject(uri.fragment()); if (eObject == null) { return null; } else { return NodeModelUtils.findActualNodeFor(eObject).getOffset(); } } }); if (offset != null) { document.replace(offset.intValue(), 0, "@Override " + separatorInfo.getLineSeparator()); } } }); } @Fix(RuleWithoutInstantiationInspector.ISSUE_CODE) public void addAction(final Issue issue, IssueResolutionAcceptor acceptor) { acceptor.accept(issue, "Add actions to ensure object creation", "Inserts the missing actions to ensure object creation.", null, new ISemanticModification() { @Override public void apply(EObject element, IModificationContext context) throws Exception { ParserRule rule = (ParserRule) element; MultiTextEdit textEdit = new MultiTextEdit(); applyToNode(rule.getAlternatives(), "{" + calculateActionName(rule) + "} ", textEdit); textEdit.apply(context.getXtextDocument()); } // algorithm to calculate the name of the action to call, including possible alias private String calculateActionName(ParserRule rule) { String actionName = rule.getName(); // rule name is default TypeRef type = rule.getType(); EClassifier classifier = type.getClassifier(); if (classifier != null) { // if there is a classifier use that instead AbstractMetamodelDeclaration metamodel = type.getMetamodel(); String alias = metamodel.getAlias(); // include alias:: if an alias for the grammar is used actionName = (alias == null ? "" : (alias + "::")) + classifier.getName(); } return actionName; } // recursive algorithm to add action to each alternative rule that is not yet instantiated private void applyToNode(AbstractElement node, String actionText, MultiTextEdit textEdit) throws BadLocationException { // reuse logic that generates the warning to analyze if we have to introduce an action Boolean isInstantiated = new RuleWithoutInstantiationInspector(null).doSwitch(node); if (isInstantiated == null || isInstantiated) { // rule is already instantiated completely, nothing to do return; } if (node instanceof Alternatives) { // there are multiple alternatives, some not instantiated, we have to handle each one separately Alternatives alternatives = (Alternatives) node; for (AbstractElement alternativeChild : alternatives.getElements()) { applyToNode(alternativeChild, actionText, textEdit); } } else { // rule without instantiation, add instantiation int offset = NodeModelUtils.findActualNodeFor(node).getOffset(); textEdit.addChild(new InsertEdit(offset, actionText)); } } }); } }