/* * Copyright 2016 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ package io.flutter; import com.google.common.base.Charsets; import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.util.ExecUtil; import com.intellij.ide.actions.ShowSettingsUtilImpl; import com.intellij.ide.impl.ProjectUtil; import com.intellij.ide.util.PropertiesComponent; import com.intellij.openapi.application.ApplicationInfo; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.roots.ModuleSourceOrderEntry; import com.intellij.openapi.roots.OrderEntry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.testFramework.LightVirtualFile; import com.intellij.util.PlatformUtils; import com.jetbrains.lang.dart.DartFileType; import com.jetbrains.lang.dart.psi.DartFile; import io.flutter.pub.PubRoot; import io.flutter.pub.PubRootCache; import io.flutter.utils.AndroidUtils; import io.flutter.utils.FlutterModuleUtils; import java.io.IOException; import java.io.InputStreamReader; import java.util.Map; import java.util.Objects; import java.util.Properties; import java.util.regex.Pattern; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.SystemIndependent; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.nodes.Tag; import org.yaml.snakeyaml.representer.Representer; import org.yaml.snakeyaml.resolver.Resolver; public class FlutterUtils { public static class FlutterPubspecInfo { private final long modificationStamp; private boolean flutter = false; private boolean plugin = false; FlutterPubspecInfo(long modificationStamp) { this.modificationStamp = modificationStamp; } public boolean declaresFlutter() { return flutter; } public boolean isFlutterPlugin() { return plugin; } public long getModificationStamp() { return modificationStamp; } } private static final Pattern VALID_ID = Pattern.compile("[_a-zA-Z$][_a-zA-Z0-9$]*"); // Note the possessive quantifiers -- greedy quantifiers are too slow on long expressions (#1421). private static final Pattern VALID_PACKAGE = Pattern.compile("^([a-z]++([_]?[a-z0-9]+)*)++$"); private FlutterUtils() { } /** * This method exists for compatibility with older IntelliJ API versions. * <p> * `Application.invokeAndWait(Runnable)` doesn't exist pre 2016.3. */ public static void invokeAndWait(@NotNull Runnable runnable) throws ProcessCanceledException { ApplicationManager.getApplication().invokeAndWait( runnable, ModalityState.defaultModalityState()); } public static boolean isFlutteryFile(@NotNull VirtualFile file) { return isDartFile(file) || PubRoot.isPubspec(file); } public static boolean couldContainWidgets(@Nullable VirtualFile file) { // Skip temp file used to show things like files downloaded from the VM. if (file instanceof LightVirtualFile) { return false; } // TODO(jacobr): we might also want to filter for files not under the current project root. return file != null && isDartFile(file); } public static boolean isDartFile(@NotNull VirtualFile file) { return Objects.equals(file.getFileType(), DartFileType.INSTANCE); } public static boolean isAndroidStudio() { return StringUtil.equals(PlatformUtils.getPlatformPrefix(), "AndroidStudio"); } /** * Write a warning message to the IntelliJ log. * <p> * This is separate from LOG.warn() to allow us to decorate the behavior. */ public static void warn(Logger logger, @NotNull Throwable t) { logger.warn(t); } /** * Write a warning message to the IntelliJ log. * <p> * This is separate from LOG.warn() to allow us to decorate the behavior. */ public static void warn(Logger logger, String message) { logger.warn(message); } /** * Write a warning message to the IntelliJ log. * <p> * This is separate from LOG.warn() to allow us to decorate the behavior. */ public static void warn(Logger logger, String message, @NotNull Throwable t) { logger.warn(message, t); } private static int getBaselineVersion() { final ApplicationInfo appInfo = ApplicationInfo.getInstance(); if (appInfo != null) { return appInfo.getBuild().getBaselineVersion(); } return -1; } public static void disableGradleProjectMigrationNotification(@NotNull Project project) { final String showMigrateToGradlePopup = "show.migrate.to.gradle.popup"; final PropertiesComponent properties = PropertiesComponent.getInstance(project); if (properties.getValue(showMigrateToGradlePopup) == null) { properties.setValue(showMigrateToGradlePopup, "false"); } } public static boolean exists(@Nullable VirtualFile file) { return file != null && file.exists(); } /** * Test if the given element is contained in a module with a pub root that declares a flutter dependency. */ public static boolean isInFlutterProject(@NotNull Project project, @NotNull PsiElement element) { final PsiFile file = element.getContainingFile(); if (file == null) { return false; } final PubRoot pubRoot = PubRootCache.getInstance(project).getRoot(file); if (pubRoot == null) { return false; } return pubRoot.declaresFlutter(); } public static boolean isInTestDir(@Nullable DartFile file) { if (file == null) return false; // Check that we're in a pub root. final PubRoot root = PubRootCache.getInstance(file.getProject()).getRoot(file.getVirtualFile().getParent()); if (root == null) return false; // Check that we're in a project path that starts with 'test/'. final String relativePath = root.getRelativePath(file.getVirtualFile()); if (relativePath == null || !relativePath.startsWith("test/")) { return false; } // Check that we're in a Flutter module. return FlutterModuleUtils.isFlutterModule(root.getModule(file.getProject())); } public static boolean isIntegrationTestingMode() { return System.getProperty("idea.required.plugins.id", "").equals("io.flutter.tests.gui.flutter-gui-tests"); } @Nullable public static VirtualFile getRealVirtualFile(@Nullable PsiFile psiFile) { return psiFile != null ? psiFile.getOriginalFile().getVirtualFile() : null; } @NotNull public static VirtualFile getProjectRoot(@NotNull Project project) { assert !project.isDefault(); @SystemIndependent String path = project.getBasePath(); assert path != null; final VirtualFile file = LocalFileSystem.getInstance().findFileByPath(path); return Objects.requireNonNull(file); } /** * Returns the Dart file for the given PsiElement, or null if not a match. */ @Nullable public static DartFile getDartFile(final @Nullable PsiElement elt) { if (elt == null) return null; final PsiFile psiFile = elt.getContainingFile(); if (!(psiFile instanceof DartFile)) return null; return (DartFile)psiFile; } public static void openFlutterSettings(@Nullable Project project) { ShowSettingsUtilImpl.showSettingsDialog(project, FlutterConstants.FLUTTER_SETTINGS_PAGE_ID, ""); } /** * Checks whether a given string is a Dart keyword. * * @param string the string to check * @return true if a keyword, false otherwise */ public static boolean isDartKeyword(@NotNull String string) { return FlutterConstants.DART_KEYWORDS.contains(string); } /** * Checks whether a given string is a valid Dart identifier. * <p> * See: https://dart.dev/guides/language/spec * * @param id the string to check * @return true if a valid identifer, false otherwise. */ public static boolean isValidDartIdentifier(@NotNull String id) { return VALID_ID.matcher(id).matches(); } /** * Checks whether a given string is a valid Dart package name. * <p> * * @param name the string to check * @return true if a valid package name, false otherwise. * @see <a href="dart.dev/tools/pub/pubspec#name">https://dart.dev/tools/pub/pubspec#name</a> */ public static boolean isValidPackageName(@NotNull String name) { return VALID_PACKAGE.matcher(name).matches(); } /** * Checks whether a given filename is an Xcode metadata file, suitable for opening externally. * * @param name the name to check * @return true if an xcode project filename */ public static boolean isXcodeFileName(@NotNull String name) { return isXcodeProjectFileName(name) || isXcodeWorkspaceFileName(name); } /** * Checks whether a given file name is an Xcode project filename. * * @param name the name to check * @return true if an xcode project filename */ public static boolean isXcodeProjectFileName(@NotNull String name) { return name.endsWith(".xcodeproj"); } /** * Checks whether a given name is an Xcode workspace filename. * * @param name the name to check * @return true if an xcode workspace filename */ public static boolean isXcodeWorkspaceFileName(@NotNull String name) { return name.endsWith(".xcworkspace"); } /** * Checks whether the given commandline executes cleanly. * * @param cmd the command * @return true if the command runs cleanly */ public static boolean runsCleanly(@NotNull GeneralCommandLine cmd) { try { return ExecUtil.execAndGetOutput(cmd).getExitCode() == 0; } catch (ExecutionException e) { return false; } } @NotNull public static PluginId getPluginId() { final PluginId pluginId = PluginId.findId("io.flutter"); assert pluginId != null; return pluginId; } /** * Returns a structured object with information about the Flutter properties of the given * pubspec file. */ public static FlutterPubspecInfo getFlutterPubspecInfo(@NotNull final VirtualFile pubspec) { // It uses Flutter if it contains 'dependencies: flutter'. // It's a plugin if it contains 'flutter: plugin'. final FlutterPubspecInfo info = new FlutterPubspecInfo(pubspec.getModificationStamp()); try { final Map<String, Object> yamlMap = readPubspecFileToMap(pubspec); if (yamlMap != null) { // Special case the 'flutter' package itself - this allows us to run their unit tests from IntelliJ. final Object packageName = yamlMap.get("name"); if ("flutter".equals(packageName)) { info.flutter = true; } // Check the dependencies. final Object dependencies = yamlMap.get("dependencies"); if (dependencies instanceof Map) { // We use `|=` for assigning to 'flutter' below as it might have been assigned to true above. info.flutter |= ((Map)dependencies).containsKey("flutter"); } // Check for a Flutter plugin. final Object flutterEntry = yamlMap.get("flutter"); if (flutterEntry instanceof Map) { info.plugin = ((Map)flutterEntry).containsKey("plugin"); } } } catch (IOException e) { // ignore } return info; } /** * Returns true if passed pubspec declares a flutter dependency. */ public static boolean declaresFlutter(@NotNull final VirtualFile pubspec) { return getFlutterPubspecInfo(pubspec).declaresFlutter(); } /** * Returns true if the passed pubspec indicates that it is a Flutter plugin. */ public static boolean isFlutterPlugin(@NotNull final VirtualFile pubspec) { return getFlutterPubspecInfo(pubspec).isFlutterPlugin(); } /** * Return the project located at the <code>path</code> or containing it. * * @param path The path to a project or one of its files * @return The Project located at the path */ @Nullable public static Project findProject(@NotNull String path) { for (Project project : ProjectManager.getInstance().getOpenProjects()) { if (ProjectUtil.isSameProject(path, project)) { return project; } } return null; } private static Map<String, Object> readPubspecFileToMap(@NotNull final VirtualFile pubspec) throws IOException { final String contents = new String(pubspec.contentsToByteArray(true /* cache contents */)); return loadPubspecInfo(contents); } private static Map<String, Object> loadPubspecInfo(@NotNull String yamlContents) { final Yaml yaml = new Yaml(new SafeConstructor(), new Representer(), new DumperOptions(), new Resolver() { @Override protected void addImplicitResolvers() { this.addImplicitResolver(Tag.BOOL, BOOL, "yYnNtTfFoO"); this.addImplicitResolver(Tag.NULL, NULL, "~nN\u0000"); this.addImplicitResolver(Tag.NULL, EMPTY, null); this.addImplicitResolver(new Tag("tag:yaml.org,2002:value"), VALUE, "="); this.addImplicitResolver(Tag.MERGE, MERGE, "<"); } }); try { //noinspection unchecked return (Map)yaml.load(yamlContents); } catch (Exception e) { return null; } } public static boolean isAndroidxProject(@NotNull Project project) { @SystemIndependent String basePath = project.getBasePath(); assert basePath != null; VirtualFile projectDir = LocalFileSystem.getInstance().findFileByPath(basePath); assert projectDir != null; VirtualFile androidDir = getFlutterManagedAndroidDir(projectDir); if (androidDir == null) { androidDir = getAndroidProjectDir(projectDir); if (androidDir == null) { return false; } } VirtualFile propFile = androidDir.findChild("gradle.properties"); if (propFile == null) { return false; } Properties properties = new Properties(); try { properties.load(new InputStreamReader(propFile.getInputStream(), Charsets.UTF_8)); } catch (IOException ex) { return false; } String value = properties.getProperty("android.useAndroidX"); if (value != null) { return Boolean.parseBoolean(value); } return false; } private static VirtualFile getAndroidProjectDir(VirtualFile dir) { return (dir.findChild("app") == null) ? null : dir; } @Nullable private static VirtualFile getFlutterManagedAndroidDir(VirtualFile dir) { VirtualFile meta = dir.findChild(".metadata"); if (meta != null) { try { Properties properties = new Properties(); properties.load(new InputStreamReader(meta.getInputStream(), Charsets.UTF_8)); String value = properties.getProperty("project_type"); if (value == null) { return null; } switch (value) { case "app": return dir.findChild("android"); case "module": return dir.findChild(".android"); case "package": return null; case "plugin": return dir.findFileByRelativePath("example/android"); } } catch (IOException e) { // fall thru } } VirtualFile android; android = dir.findChild(".android"); if (android != null) { return android; } android = dir.findChild("android"); if (android != null) { return android; } android = dir.findFileByRelativePath("example/android"); if (android != null) { return android; } return null; } @Nullable public static Module findModuleNamed(@NotNull Project project, @NotNull String name) { Module[] modules = ModuleManager.getInstance(project).getModules(); for (Module module : modules) { if (module.getName().equals(name)) { return module; } } return null; } public static String flutterGradleModuleName(Project project) { return project.getName().replaceAll(" ", "_") + "." + AndroidUtils.FLUTTER_MODULE_NAME; } @Nullable public static Module findFlutterGradleModule(@NotNull Project project) { String moduleName = AndroidUtils.FLUTTER_MODULE_NAME; Module module = findModuleNamed(project, moduleName); if (module == null) { moduleName = flutterGradleModuleName(project); module = findModuleNamed(project, moduleName); if (module == null) { return null; } } VirtualFile file = locateModuleRoot(module); if (file == null) { return null; } file = file.getParent().getParent(); VirtualFile meta = file.findChild(".metadata"); if (meta == null) { return null; } VirtualFile android = getFlutterManagedAndroidDir(meta.getParent()); if (android != null && android.getName().equals(".android")) { return module; // Only true for Flutter modules. } return null; } @Nullable public static VirtualFile locateModuleRoot(@NotNull Module module) { ModuleSourceOrderEntry entry = findModuleSourceEntry(module); if (entry == null) return null; VirtualFile[] roots = entry.getRootModel().getContentRoots(); if (roots.length == 0) return null; return roots[0]; } @Nullable private static ModuleSourceOrderEntry findModuleSourceEntry(@NotNull Module module) { ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module); OrderEntry[] orderEntries = moduleRootManager.getOrderEntries(); for (OrderEntry entry : orderEntries) { if (entry instanceof ModuleSourceOrderEntry) { return (ModuleSourceOrderEntry)entry; } } return null; } }