package com.jetbrains.lang.dart.ide.inspections; import com.intellij.CommonBundle; import com.intellij.codeInspection.IntentionAndQuickFixAction; import com.intellij.codeInspection.LocalInspectionTool; import com.intellij.codeInspection.ProblemHighlightType; import com.intellij.codeInspection.ProblemsHolder; import com.intellij.notification.Notification; import com.intellij.notification.NotificationListener; import com.intellij.notification.NotificationType; import com.intellij.notification.Notifications; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.options.ShowSettingsUtil; import com.intellij.openapi.progress.ProgressIndicatorProvider; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectBundle; import com.intellij.openapi.roots.ModifiableRootModel; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference; import com.intellij.util.PlatformUtils; import com.jetbrains.lang.dart.DartBundle; import com.jetbrains.lang.dart.DartProjectComponent; import com.jetbrains.lang.dart.ide.actions.DartPubGetAction; import com.jetbrains.lang.dart.psi.PubspecYamlReferenceContributor; import com.jetbrains.lang.dart.sdk.DartSdk; import com.jetbrains.lang.dart.sdk.DartSdkLibUtil; import com.jetbrains.lang.dart.util.DartResolveUtil; import com.jetbrains.lang.dart.util.PubspecYamlUtil; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.yaml.psi.YAMLKeyValue; import javax.swing.event.HyperlinkEvent; public class DartPathPackageReferenceInspection extends LocalInspectionTool { private static final String GROUP_DISPLAY_ID = "pubspec.yaml inspection"; @Override @NotNull public String getGroupDisplayName() { return DartBundle.message("inspections.group.name"); } @Override @Nls @NotNull public String getDisplayName() { return DartBundle.message("path.package.reference.inspection.name"); } @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, final boolean isOnTheFly) { if (!PubspecYamlUtil.PUBSPEC_YAML.equals(holder.getFile().getName())) return super.buildVisitor(holder, isOnTheFly); final Module module = ModuleUtilCore.findModuleForPsiElement(holder.getFile()); final DartSdk sdk = DartSdk.getDartSdk(holder.getProject()); if (module == null || sdk == null || !DartSdkLibUtil.isDartSdkEnabled(module)) { return super.buildVisitor(holder, isOnTheFly); } return new PsiElementVisitor() { @Override public void visitElement(final PsiElement element) { ProgressIndicatorProvider.checkCanceled(); if (!(element instanceof YAMLKeyValue) || !PubspecYamlReferenceContributor.isPathPackageDefinition((YAMLKeyValue)element) || ((YAMLKeyValue)element).getValue() == null) { return; } final VirtualFile packageDir = checkReferences(holder, (YAMLKeyValue)element); if (packageDir == null) { return; } if (packageDir.findChild(PubspecYamlUtil.PUBSPEC_YAML) == null) { final String message = DartBundle.message("pubspec.yaml.not.found.in", FileUtil.toSystemDependentName(packageDir.getPath())); holder.registerProblem(((YAMLKeyValue)element).getValue(), message); return; } final VirtualFile file = DartResolveUtil.getRealVirtualFile(element.getContainingFile()); if (file != null && packageDir.equals(file.getParent())) { holder.registerProblem(((YAMLKeyValue)element).getValue(), DartBundle.message("path.package.reference.to.itself")); return; } final VirtualFile libDir = packageDir.findChild(PubspecYamlUtil.LIB_DIR_NAME); if (libDir != null && libDir.isDirectory() && !ProjectRootManager.getInstance(element.getProject()).getFileIndex().isInContent(libDir)) { final String message = DartBundle.message("folder.0.not.in.project.content", FileUtil.toSystemDependentName(packageDir.getPath())); holder.registerProblem(((YAMLKeyValue)element).getValue(), message, new AddContentRootFix(module, packageDir)); } } }; } @Nullable private static VirtualFile checkReferences(@NotNull final ProblemsHolder holder, @NotNull final YAMLKeyValue element) { for (PsiReference reference : element.getReferences()) { if (reference instanceof FileReference && !reference.isSoft()) { final PsiFileSystemItem resolve = ((FileReference)reference).resolve(); if (resolve == null) { holder.registerProblem(reference.getElement(), ((FileReference)reference).getUnresolvedMessagePattern(), ProblemHighlightType.GENERIC_ERROR, reference.getRangeInElement()); return null; } else if (((FileReference)reference).isLast()) { final VirtualFile dir = resolve.getVirtualFile(); if (dir != null && dir.isDirectory()) { return dir; } } } } return null; } private static class AddContentRootFix extends IntentionAndQuickFixAction { @NotNull private final Module myModule; @NotNull private final VirtualFile myContentRoot; private AddContentRootFix(@NotNull final Module module, @NotNull final VirtualFile contentRoot) { myModule = module; myContentRoot = contentRoot; } @Override @NotNull public String getName() { return DartBundle.message("configure.folder.0.as.content.root", FileUtil.toSystemDependentName(myContentRoot.getPath())); } @Override @NotNull public String getFamilyName() { return DartBundle.message("configure.folder.as.content.root"); } @Override public boolean startInWriteAction() { return false; } @Override public void applyFix(@NotNull final Project project, final PsiFile psiFile, @Nullable final Editor editor) { try { checkCanAddContentRoot(myModule, myContentRoot); } catch (Exception e) { showErrorDialog(myModule, e); return; } final ModifiableRootModel modifiableModel = ModuleRootManager.getInstance(myModule).getModifiableModel(); try { modifiableModel.addContentEntry(myContentRoot); ApplicationManager.getApplication().runWriteAction(modifiableModel::commit); final VirtualFile otherPubspec = myContentRoot.findChild(PubspecYamlUtil.PUBSPEC_YAML); if (otherPubspec != null) { // exclude before indexing started DartProjectComponent.excludeBuildAndPackagesFolders(myModule, otherPubspec); final AnAction pubGetAction = ActionManager.getInstance().getAction("Dart.pub.get"); if (pubGetAction instanceof DartPubGetAction) { ((DartPubGetAction)pubGetAction).performPubAction(myModule, otherPubspec, false); } } showSuccessNotification(myModule, myContentRoot); } catch (Exception e) { showErrorDialog(myModule, e); } finally { if (!modifiableModel.isDisposed()) { modifiableModel.dispose(); } } final VirtualFile yamlFile = DartResolveUtil.getRealVirtualFile(psiFile); if (yamlFile != null && PubspecYamlUtil.PUBSPEC_YAML.equals(yamlFile.getName())) { DartProjectComponent.excludeBuildAndPackagesFolders(myModule, yamlFile); } } private static void showSuccessNotification(@NotNull final Module module, @NotNull final VirtualFile root) { final String title = DartBundle.message("content.root.added.title"); final String message = DartSdkLibUtil.isIdeWithMultipleModuleSupport() ? DartBundle.message("content.root.added.to.module", module.getName(), FileUtil.toSystemDependentName(root.getPath())) : DartBundle.message("content.root.added.to.project", FileUtil.toSystemDependentName(root.getPath()), CommonBundle.settingsTitle(), getProjectRootsConfigurableName()); Notifications.Bus.notify(new Notification(GROUP_DISPLAY_ID, title, message, NotificationType.INFORMATION, new NotificationListener.Adapter() { @Override protected void hyperlinkActivated(@NotNull final Notification notification, @NotNull final HyperlinkEvent e) { openProjectRootsConfigurable(module); } })); } private static void showErrorDialog(@NotNull final Module module, @NotNull final Exception e) { final String message = DartBundle.message("can.not.add.content.root", e.getMessage()); final String title = DartBundle.message("add.content.root.title"); final String okText = DartBundle.message("configure.project.roots"); final String cancelText = CommonBundle.getCancelButtonText(); final int choice = Messages.showOkCancelDialog(module.getProject(), message, title, okText, cancelText, Messages.getWarningIcon()); if (choice == Messages.OK) { openProjectRootsConfigurable(module); } } // similar to com.intellij.openapi.roots.ui.configuration.CommonContentEntriesEditor.AddContentEntryAction.validateContentEntriesCandidates() private static void checkCanAddContentRoot(@NotNull final Module module, @NotNull final VirtualFile contentRoot) throws Exception { for (final VirtualFile contentEntryFile : ModuleRootManager.getInstance(module).getContentRoots()) { if (contentEntryFile.equals(contentRoot)) { throw new Exception(ProjectBundle.message("module.paths.add.content.already.exists.error", contentRoot.getPresentableUrl())); } if (VfsUtilCore.isAncestor(contentEntryFile, contentRoot, true)) { // intersection not allowed throw new Exception( ProjectBundle.message("module.paths.add.content.intersect.error", contentRoot.getPresentableUrl(), contentEntryFile.getPresentableUrl())); } if (VfsUtilCore.isAncestor(contentRoot, contentEntryFile, true)) { // intersection not allowed throw new Exception( ProjectBundle.message("module.paths.add.content.dominate.error", contentRoot.getPresentableUrl(), contentEntryFile.getPresentableUrl())); } } for (final Module otherModule : ModuleManager.getInstance(module.getProject()).getModules()) { if (module.equals(otherModule)) { continue; } for (VirtualFile moduleContentRoot : ModuleRootManager.getInstance(otherModule).getContentRoots()) { if (contentRoot.equals(moduleContentRoot)) { throw new Exception( ProjectBundle.message("module.paths.add.content.duplicate.error", contentRoot.getPresentableUrl(), otherModule.getName())); } } } } } private static void openProjectRootsConfigurable(final Module module) { if (PlatformUtils.isWebStorm() || PlatformUtils.isPhpStorm() || PlatformUtils.isPyCharm() || PlatformUtils.isRubyMine()) { ShowSettingsUtil.getInstance().showSettingsDialog(module.getProject(), getProjectRootsConfigurableName()); } else { ProjectSettingsService.getInstance(module.getProject()).openContentEntriesSettings(module); } } @SuppressWarnings("IfStatementWithIdenticalBranches") private static String getProjectRootsConfigurableName() { if (PlatformUtils.isWebStorm() || PlatformUtils.isPhpStorm()) { // "Directories" comes from com.intellij.webcore.resourceRoots.WebIdeProjectStructureConfigurable.getDisplayName() return "Directories"; } else if (PlatformUtils.isRubyMine()) { // "Project Structure" comes from org.jetbrains.plugins.ruby.settings.RubyProjectStructureConfigurable.getDisplayName() return "Project Structure"; } else if (PlatformUtils.isPyCharm()) { // "Project Structure" comes from com.jetbrains.python.configuration.PyContentEntriesModuleConfigurable.getDisplayName() return "Project Structure"; } // com.intellij.openapi.roots.ui.configuration.PlatformContentEntriesConfigurable.getDisplayName() return "Project Structure"; } }