diff --git a/Dart/src/com/jetbrains/lang/dart/DartFileListener.java b/Dart/src/com/jetbrains/lang/dart/DartFileListener.java index 7782a65e30a..000b51adf1e 100644 --- a/Dart/src/com/jetbrains/lang/dart/DartFileListener.java +++ b/Dart/src/com/jetbrains/lang/dart/DartFileListener.java @@ -68,8 +68,12 @@ public ChangeApplier prepareChange(@NotNull List if (event instanceof VFilePropertyChangeEvent) { if (((VFilePropertyChangeEvent)event).isRename()) { - if (DotPackagesFileUtil.DOT_PACKAGES.equals(((VFilePropertyChangeEvent)event).getOldValue()) || - DotPackagesFileUtil.DOT_PACKAGES.equals(((VFilePropertyChangeEvent)event).getNewValue())) { + if ( + DotPackagesFileUtil.PACKAGE_CONFIG_JSON.equals(((VFilePropertyChangeEvent)event).getOldValue()) || + DotPackagesFileUtil.PACKAGE_CONFIG_JSON.equals(((VFilePropertyChangeEvent)event).getNewValue()) || + DotPackagesFileUtil.DOT_PACKAGES.equals(((VFilePropertyChangeEvent)event).getOldValue()) || + DotPackagesFileUtil.DOT_PACKAGES.equals(((VFilePropertyChangeEvent)event).getNewValue()) + ) { dotPackageEvents.add(event); } @@ -80,7 +84,10 @@ public ChangeApplier prepareChange(@NotNull List } } else { - if (DotPackagesFileUtil.DOT_PACKAGES.equals(PathUtil.getFileName(event.getPath()))) { + if ( + DotPackagesFileUtil.PACKAGE_CONFIG_JSON.equals(PathUtil.getFileName(event.getPath())) || + DotPackagesFileUtil.DOT_PACKAGES.equals(PathUtil.getFileName(event.getPath())) + ) { dotPackageEvents.add(event); } @@ -155,9 +162,9 @@ private static DartLibInfo collectPackagesLibraryRoots(@NotNull final Project pr final Module module = fileIndex.getModuleForFile(pubspecFile); if (module == null || !DartSdkLibUtil.isDartSdkEnabled(module)) continue; - final VirtualFile dotPackagesFile = findDotPackagesFile(pubspecFile.getParent()); - if (dotPackagesFile != null) { - final Map packagesMap = DotPackagesFileUtil.getPackagesMap(dotPackagesFile); + final VirtualFile packageConfigJsonFile = DotPackagesFileUtil.findPackageConfigJsonFile(pubspecFile.getParent()); + if (packageConfigJsonFile != null) { + final Map packagesMap = DotPackagesFileUtil.getPackagesMapFromPackageConfigJsonFile(packageConfigJsonFile); if (packagesMap != null) { for (Map.Entry entry : packagesMap.entrySet()) { final String packageName = entry.getKey(); @@ -167,24 +174,26 @@ private static DartLibInfo collectPackagesLibraryRoots(@NotNull final Project pr } } } + } else { + final VirtualFile dotPackagesFile = DotPackagesFileUtil.findDotPackagesFile(pubspecFile.getParent()); + if (dotPackagesFile != null) { + final Map packagesMap = DotPackagesFileUtil.getPackagesMap(dotPackagesFile); + if (packagesMap != null) { + for (Map.Entry entry : packagesMap.entrySet()) { + final String packageName = entry.getKey(); + final String packagePath = entry.getValue(); + if (isPathOutsideProjectContent(fileIndex, packagePath)) { + libInfo.addPackage(packageName, packagePath); + } + } + } + } } } return libInfo; } - @Nullable - private static VirtualFile findDotPackagesFile(@Nullable VirtualFile dir) { - while (dir != null) { - final VirtualFile file = dir.findChild(DotPackagesFileUtil.DOT_PACKAGES); - if (file != null && !file.isDirectory()) { - return file; - } - dir = dir.getParent(); - } - return null; - } - @NotNull public static Library updatePackagesLibraryRoots(@NotNull final Project project, @NotNull final DartLibInfo libInfo) { final LibraryTable projectLibraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable(project); diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisServerService.java b/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisServerService.java index 42c9efaa79f..6e93d14d1a2 100644 --- a/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisServerService.java +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisServerService.java @@ -83,6 +83,10 @@ public final class DartAnalysisServerService implements Disposable { // https://github.com/dart-lang/webdev/blob/master/webdev/pubspec.yaml#L11 public static final String MIN_WEBDEV_SDK_VERSION = "2.6.0"; + // As of the Dart SDK version 2.8.0, the file .dart_tool/package_config.json is preferred over the .packages file. + // https://github.com/dart-lang/sdk/issues/48272 + public static final String MIN_PACKAGE_CONFIG_JSON_SDK_VERSION = "2.8.0"; + // The dart cli command provides a language server command, `dart language-server`, which // should be used going forward instead of `dart .../analysis_server.dart.snapshot`. public static final String MIN_DART_LANG_SERVER_SDK_VERSION = "2.16.0"; @@ -459,6 +463,10 @@ public static boolean isDartSdkVersionSufficientForWebdev(@NotNull final DartSdk return StringUtil.compareVersionNumbers(sdk.getVersion(), MIN_WEBDEV_SDK_VERSION) >= 0; } + public static boolean isDartSdkVersionSufficientForPackageConfigJson(@NotNull final DartSdk sdk) { + return StringUtil.compareVersionNumbers(sdk.getVersion(), MIN_PACKAGE_CONFIG_JSON_SDK_VERSION) >= 0; + } + public static boolean isDartSdkVersionSufficientForDartLangServer(@NotNull final DartSdk sdk) { return StringUtil.compareVersionNumbers(sdk.getVersion(), MIN_DART_LANG_SERVER_SDK_VERSION) >= 0; } diff --git a/Dart/src/com/jetbrains/lang/dart/ide/inspections/DartOutdatedDependenciesInspection.java b/Dart/src/com/jetbrains/lang/dart/ide/inspections/DartOutdatedDependenciesInspection.java index 10485520aca..9f5e05c0939 100644 --- a/Dart/src/com/jetbrains/lang/dart/ide/inspections/DartOutdatedDependenciesInspection.java +++ b/Dart/src/com/jetbrains/lang/dart/ide/inspections/DartOutdatedDependenciesInspection.java @@ -19,6 +19,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.jetbrains.lang.dart.DartBundle; +import com.jetbrains.lang.dart.analyzer.DartAnalysisServerService; import com.jetbrains.lang.dart.flutter.FlutterUtil; import com.jetbrains.lang.dart.ide.actions.DartPubActionBase; import com.jetbrains.lang.dart.psi.DartFile; @@ -67,14 +68,28 @@ public final class DartOutdatedDependenciesInspection extends LocalInspectionToo if (FlutterUtil.isPubspecDeclaringFlutter(pubspecFile)) return null; // 'pub get' will fail anyway - final VirtualFile dotPackagesFile = pubspecFile.getParent().findChild(DotPackagesFileUtil.DOT_PACKAGES); + if (DartAnalysisServerService.isDartSdkVersionSufficientForPackageConfigJson(sdk)) { + final VirtualFile packageConfigJsonFile = DotPackagesFileUtil.getPackageConfigJsonFile(pubspecFile); - if (dotPackagesFile == null) { - return createProblemDescriptors(manager, psiFile, pubspecFile, DartBundle.message("pub.get.never.done")); + if (packageConfigJsonFile == null) { + return createProblemDescriptors(manager, psiFile, pubspecFile, DartBundle.message("pub.get.never.done")); + } + + if (FileDocumentManager.getInstance().isFileModified(pubspecFile) || + pubspecFile.getTimeStamp() > packageConfigJsonFile.getTimeStamp()) { + return createProblemDescriptors(manager, psiFile, pubspecFile, DartBundle.message("pubspec.edited")); + } } + else { + final VirtualFile dotPackagesFile = pubspecFile.getParent().findChild(DotPackagesFileUtil.DOT_PACKAGES); - if (FileDocumentManager.getInstance().isFileModified(pubspecFile) || pubspecFile.getTimeStamp() > dotPackagesFile.getTimeStamp()) { - return createProblemDescriptors(manager, psiFile, pubspecFile, DartBundle.message("pubspec.edited")); + if (dotPackagesFile == null) { + return createProblemDescriptors(manager, psiFile, pubspecFile, DartBundle.message("pub.get.never.done")); + } + + if (FileDocumentManager.getInstance().isFileModified(pubspecFile) || pubspecFile.getTimeStamp() > dotPackagesFile.getTimeStamp()) { + return createProblemDescriptors(manager, psiFile, pubspecFile, DartBundle.message("pubspec.edited")); + } } return null; diff --git a/Dart/src/com/jetbrains/lang/dart/psi/DartPackageAwareFileReference.java b/Dart/src/com/jetbrains/lang/dart/psi/DartPackageAwareFileReference.java index 68ae535ba79..65aa4f2a9cf 100644 --- a/Dart/src/com/jetbrains/lang/dart/psi/DartPackageAwareFileReference.java +++ b/Dart/src/com/jetbrains/lang/dart/psi/DartPackageAwareFileReference.java @@ -24,10 +24,10 @@ class DartPackageAwareFileReference extends FileReference { @NotNull private final DartUrlResolver myDartResolver; DartPackageAwareFileReference(@NotNull final FileReferenceSet fileReferenceSet, - final TextRange range, - final int index, - final String text, - @NotNull final DartUrlResolver dartResolver) { + final TextRange range, + final int index, + final String text, + @NotNull final DartUrlResolver dartResolver) { super(fileReferenceSet, range, index, text); myDartResolver = dartResolver; } @@ -41,10 +41,23 @@ class DartPackageAwareFileReference extends FileReference { if (psiDirectory != null) { return new ResolveResult[]{new PsiElementResolveResult(psiDirectory)}; } - VirtualFile dotPackages = pubspecYamlFile == null ? null : pubspecYamlFile.getParent().findChild(DotPackagesFileUtil.DOT_PACKAGES); - final PsiFile psiFile = dotPackages == null ? null : containingFile.getManager().findFile(dotPackages); - if (psiFile != null) { - return new ResolveResult[]{new PsiElementResolveResult(psiFile)}; + + // Starting in Dart 2.8.0, the .packages file is deprecated in favor of .dart_tool/package_config.json, this code preserves the + // older .packages functionality for older projects, but now looks for the .dart_tool/package_config.json file first. + VirtualFile packagesConfigJsonFile = pubspecYamlFile == null ? null : DotPackagesFileUtil.getPackageConfigJsonFile(pubspecYamlFile); + if (packagesConfigJsonFile != null) { + final PsiFile psiFile = containingFile.getManager().findFile(packagesConfigJsonFile); + if (psiFile != null) { + return new ResolveResult[]{new PsiElementResolveResult(psiFile)}; + } + } + else { + // Pre-.packages deprecation fallback for older projects and SDK usages + VirtualFile dotPackages = pubspecYamlFile == null ? null : pubspecYamlFile.getParent().findChild(DotPackagesFileUtil.DOT_PACKAGES); + final PsiFile psiFile = dotPackages == null ? null : containingFile.getManager().findFile(dotPackages); + if (psiFile != null) { + return new ResolveResult[]{new PsiElementResolveResult(psiFile)}; + } } } diff --git a/Dart/src/com/jetbrains/lang/dart/util/DartUrlResolverImpl.java b/Dart/src/com/jetbrains/lang/dart/util/DartUrlResolverImpl.java index 9cff9519be9..e578173da92 100644 --- a/Dart/src/com/jetbrains/lang/dart/util/DartUrlResolverImpl.java +++ b/Dart/src/com/jetbrains/lang/dart/util/DartUrlResolverImpl.java @@ -207,10 +207,28 @@ private static String getUrlIfFileFromDartPackagesLib(final @NotNull VirtualFile private void initLivePackageNameToDirMap() { final VirtualFile baseDir = myPubspecYamlFile == null ? null : myPubspecYamlFile.getParent(); - if (myPubspecYamlFile == null || baseDir == null) return; - final VirtualFile dotPackagesFile = baseDir.findChild(DotPackagesFileUtil.DOT_PACKAGES); + if (myPubspecYamlFile == null || baseDir == null) { + return; + } - if (dotPackagesFile != null && !dotPackagesFile.isDirectory()) { + final VirtualFile packageConfigJsonFile = DotPackagesFileUtil.getPackageConfigJsonFile(baseDir); + final VirtualFile dotPackagesFile = baseDir.findChild(DotPackagesFileUtil.DOT_PACKAGES); + if (packageConfigJsonFile != null && !packageConfigJsonFile.isDirectory()) { + // Use the generated .dart_tool/package_config.json if it exists. + final Map packagesMap = DotPackagesFileUtil.getPackagesMapFromPackageConfigJsonFile(packageConfigJsonFile); + if (packagesMap != null) { + for (Map.Entry entry : packagesMap.entrySet()) { + final String packageName = entry.getKey(); + final String packagePath = entry.getValue(); + final VirtualFile packageDir = myPubspecYamlFile.getFileSystem().findFileByPath(packagePath); + if (packageDir != null) { + myLivePackageNameToDirMap.put(packageName, packageDir); + } + } + } + } + else if (dotPackagesFile != null && !dotPackagesFile.isDirectory()) { + // Otherwise, fall back to the .packages file. final Map packagesMap = DotPackagesFileUtil.getPackagesMap(dotPackagesFile); if (packagesMap != null) { for (Map.Entry entry : packagesMap.entrySet()) { diff --git a/Dart/src/com/jetbrains/lang/dart/util/DotPackagesFileUtil.java b/Dart/src/com/jetbrains/lang/dart/util/DotPackagesFileUtil.java index bc4f06385f0..479d33a9768 100644 --- a/Dart/src/com/jetbrains/lang/dart/util/DotPackagesFileUtil.java +++ b/Dart/src/com/jetbrains/lang/dart/util/DotPackagesFileUtil.java @@ -1,6 +1,10 @@ // Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.lang.dart.util; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; @@ -15,6 +19,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -22,8 +27,141 @@ public final class DotPackagesFileUtil { public static final String DOT_PACKAGES = ".packages"; + public static final String DART_TOOL_DIR = ".dart_tool"; + public static final String PACKAGE_CONFIG_JSON = "package_config.json"; + private static final Key>> MOD_STAMP_TO_PACKAGES_MAP = Key.create("MOD_STAMP_TO_PACKAGES_MAP"); + /** + * Starting with some {@link VirtualFile}, search the parent directories for some instance of .dart_tool/package_config.json. + */ + @Nullable + public static VirtualFile findPackageConfigJsonFile(@Nullable final VirtualFile vFile) { + if (vFile == null) { + return null; + } + VirtualFile dir = vFile.isDirectory() ? vFile : vFile.getParent(); + while (dir != null) { + VirtualFile dartToolDir = dir.findChild(DART_TOOL_DIR); + if (dartToolDir != null && dartToolDir.isDirectory()) { + return dartToolDir.findChild(PACKAGE_CONFIG_JSON); + } + dir = dir.getParent(); + } + return null; + } + + /** + * Given a file Dart pub root (either the yaml pubspec {@link VirtualFile} or the parent of a yaml pubspec), return the + * .dart_tool/package_config.json {@link VirtualFile}, if it exists. + */ + @Nullable + public static VirtualFile getPackageConfigJsonFile(@Nullable final VirtualFile vFile) { + if (vFile == null) { + return null; + } + VirtualFile dir = vFile.isDirectory() ? vFile : vFile.getParent(); + VirtualFile dartToolDir = dir.findChild(DART_TOOL_DIR); + if (dartToolDir != null && dartToolDir.isDirectory()) { + return dartToolDir.findChild(PACKAGE_CONFIG_JSON); + } + return null; + } + + @Nullable + public static VirtualFile findDotPackagesFile(@Nullable VirtualFile dir) { + while (dir != null) { + final VirtualFile file = dir.findChild(DOT_PACKAGES); + if (file != null && !file.isDirectory()) { + return file; + } + dir = dir.getParent(); + } + return null; + } + + @Nullable + public static Map getPackagesMapFromPackageConfigJsonFile(@NotNull final VirtualFile packageConfigJsonFile) { + Pair> data = packageConfigJsonFile.getUserData(MOD_STAMP_TO_PACKAGES_MAP); + + final Long currentTimestamp = packageConfigJsonFile.getModificationCount(); + final Long cachedTimestamp = Pair.getFirst(data); + + if (cachedTimestamp == null || !cachedTimestamp.equals(currentTimestamp)) { + data = null; + packageConfigJsonFile.putUserData(MOD_STAMP_TO_PACKAGES_MAP, null); + final Map packagesMap = packagesMapFromJson(packageConfigJsonFile); + + if (packagesMap != null) { + data = Pair.create(currentTimestamp, packagesMap); + packageConfigJsonFile.putUserData(MOD_STAMP_TO_PACKAGES_MAP, data); + } + } + + return Pair.getSecond(data); + } + + /** + * Example JSON parsed: + * ``` + * { + * "configVersion": 2, + * "packages": [ + * { + * "name": "pedantic", + * "rootUri": "file:///Users/jwren/.pub-cache/hosted/pub.dartlang.org/pedantic-1.11.0", + * "packageUri": "lib/", + * "languageVersion": "2.12" + * }, + * { + * "name": "console_dart", + * "rootUri": "../", + * "packageUri": "lib/", + * "languageVersion": "2.12" + * } + * ], + * "generated": "2022-03-04T22:10:17.132325Z", + * "generator": "pub", + * "generatorVersion": "2.17.0-edge.f9147d933ef019c7e304c19ac039f57226ce1e37" + * } + * ``` + */ + @Nullable + private static Map packagesMapFromJson(@NotNull final VirtualFile packageConfigJsonFile) { + String fileContentsStr = FileUtil.loadFileOrNull(packageConfigJsonFile.getPath()); + if (fileContentsStr == null) { + return null; + } + + final JsonElement jsonElement = JsonParser.parseString(fileContentsStr); + if (jsonElement instanceof JsonObject && + ((JsonObject)jsonElement).get("packages") != null && + ((JsonObject)jsonElement).get("packages").isJsonArray()) { + final Map result = new HashMap<>(); + final JsonArray jsonArray = ((JsonObject)jsonElement).get("packages").getAsJsonArray(); + for (JsonElement element : jsonArray) { + JsonObject jsonObjectPackage = element.getAsJsonObject(); + if (jsonObjectPackage.get("name") != null && + jsonObjectPackage.get("rootUri") != null && + jsonObjectPackage.get("packageUri") != null + ) { + final String packageName = jsonObjectPackage.get("name").getAsString(); + final String rootUriValue = jsonObjectPackage.get("rootUri").getAsString(); + final String packageUriValue = jsonObjectPackage.get("packageUri").getAsString(); + // need to protect '+' chars because URLDecoder.decode replaces '+' with space + final String encodedUriWithoutPluses = StringUtil.replace(rootUriValue + "/" + packageUriValue, "+", "%2B"); + final String uri = URLUtil.decode(encodedUriWithoutPluses); + final String packageUri = getAbsolutePackageRootPath(packageConfigJsonFile.getParent().getParent(), uri); + if (!packageName.isEmpty() && packageUri != null) { + result.put(packageName, packageUri); + } + } + } + return result; + } + return null; + } + @Nullable public static Map getPackagesMap(@NotNull final VirtualFile dotPackagesFile) { Pair> data = dotPackagesFile.getUserData(MOD_STAMP_TO_PACKAGES_MAP);