package com.jetbrains.lang.dart.util;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.PairConsumer;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
public class PubspecYamlUtil {
public static final String PUBSPEC_YAML = "pubspec.yaml";
private static final String NAME = "name";
public static final String DEPENDENCIES = "dependencies";
public static final String DEV_DEPENDENCIES = "dev_dependencies";
public static final String DEPENDENCY_OVERRIDES = "dependency_overrides";
public static final String PATH = "path";
public static final String LIB_DIR_NAME = "lib";
private static final Key<Pair<Long, Map<String, Object>>> MOD_STAMP_TO_PUBSPEC_NAME = Key.create("MOD_STAMP_TO_PUBSPEC_NAME");
@Nullable
public static VirtualFile findPubspecYamlFile(@NotNull final Project project, @NotNull final VirtualFile contextFile) {
final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(project).getFileIndex();
VirtualFile current = contextFile;
VirtualFile parent = contextFile.isDirectory() ? contextFile : contextFile.getParent();
while (parent != null && (LIB_DIR_NAME.equals(current.getName()) || fileIndex.isInContent(parent))) {
current = parent;
final VirtualFile file = parent.findChild(PUBSPEC_YAML);
if (file != null && !file.isDirectory()) {
return file;
}
parent = current.getParent();
}
return null;
}
@Nullable
public static String getDartProjectName(@NotNull final VirtualFile pubspecYamlFile) {
final Map<String, Object> yamlInfo = getPubspecYamlInfo(pubspecYamlFile);
final Object name = yamlInfo == null ? null : yamlInfo.get(NAME);
return name instanceof String ? (String)name : null;
}
public static void processInProjectPathPackagesRecursively(@NotNull final Project project,
@NotNull final VirtualFile pubspecYamlFile,
@NotNull final PairConsumer<String, VirtualFile> pathPackageNameAndDirConsumer) {
processInProjectPathPackagesRecursively(project, pubspecYamlFile, new THashSet<>(), pathPackageNameAndDirConsumer);
}
private static void processInProjectPathPackagesRecursively(@NotNull final Project project,
@NotNull final VirtualFile pubspecYamlFile,
@NotNull final Set<VirtualFile> processedPubspecs,
@NotNull final PairConsumer<String, VirtualFile> pathPackageNameAndDirConsumer) {
if (!processedPubspecs.add(pubspecYamlFile)) return;
final VirtualFile baseDir = pubspecYamlFile.getParent();
final Map<String, Object> yamlInfo = getPubspecYamlInfo(pubspecYamlFile);
if (baseDir == null || yamlInfo == null) return;
processYamlDepsRecursively(project, processedPubspecs, pathPackageNameAndDirConsumer, baseDir, yamlInfo.get(DEPENDENCIES));
processYamlDepsRecursively(project, processedPubspecs, pathPackageNameAndDirConsumer, baseDir, yamlInfo.get(DEV_DEPENDENCIES));
processYamlDepsRecursively(project, processedPubspecs, pathPackageNameAndDirConsumer, baseDir, yamlInfo.get(DEPENDENCY_OVERRIDES));
}
// Path packages: https://www.dartlang.org/tools/pub/dependencies.html#path-packages
private static void processYamlDepsRecursively(@NotNull final Project project,
@NotNull final Set<VirtualFile> processedPubspecs,
@NotNull final PairConsumer<String, VirtualFile> pathPackageNameAndRelPathConsumer,
@NotNull final VirtualFile baseDir,
@Nullable final Object yamlDep) {
// see com.google.dart.tools.core.pub.PubspecModel#processDependencies
if (!(yamlDep instanceof Map)) return;
//noinspection unchecked
for (Map.Entry<String, Object> packageEntry : ((Map<String, Object>)yamlDep).entrySet()) {
final String packageName = packageEntry.getKey();
final Object packageEntryValue = packageEntry.getValue();
if (packageEntryValue instanceof Map) {
final Object pathObj = ((Map)packageEntryValue).get(PATH);
if (pathObj instanceof String) {
final VirtualFile packageFolder = VfsUtilCore.findRelativeFile(pathObj + "/" + LIB_DIR_NAME, baseDir);
if (packageFolder != null &&
packageFolder.isDirectory() &&
ProjectRootManager.getInstance(project).getFileIndex().isInContent(packageFolder)) {
pathPackageNameAndRelPathConsumer.consume(packageName, packageFolder);
final VirtualFile otherPubspecYaml = packageFolder.getParent().findChild(PUBSPEC_YAML);
if (otherPubspecYaml != null && !otherPubspecYaml.isDirectory()) {
processInProjectPathPackagesRecursively(project, otherPubspecYaml, processedPubspecs, pathPackageNameAndRelPathConsumer);
}
}
}
}
}
}
@Nullable
private static Map<String, Object> getPubspecYamlInfo(final @NotNull VirtualFile pubspecYamlFile) {
// do not use Yaml plugin here - IntelliJ IDEA Community Edition doesn't contain it.
Pair<Long, Map<String, Object>> data = pubspecYamlFile.getUserData(MOD_STAMP_TO_PUBSPEC_NAME);
final FileDocumentManager documentManager = FileDocumentManager.getInstance();
final Document cachedDocument = documentManager.getCachedDocument(pubspecYamlFile);
final Long currentTimestamp = cachedDocument != null ? cachedDocument.getModificationStamp() : pubspecYamlFile.getModificationCount();
final Long cachedTimestamp = data == null ? null : data.first;
if (cachedTimestamp == null || !cachedTimestamp.equals(currentTimestamp)) {
data = null;
pubspecYamlFile.putUserData(MOD_STAMP_TO_PUBSPEC_NAME, null);
try {
final Map<String, Object> pubspecYamlInfo;
if (cachedDocument != null) {
pubspecYamlInfo = loadPubspecYamlInfo(cachedDocument.getText());
}
else {
pubspecYamlInfo = loadPubspecYamlInfo(VfsUtilCore.loadText(pubspecYamlFile));
}
if (pubspecYamlInfo != null) {
data = Pair.create(currentTimestamp, pubspecYamlInfo);
pubspecYamlFile.putUserData(MOD_STAMP_TO_PUBSPEC_NAME, data);
}
}
catch (IOException ignored) {/* unlucky */}
}
return data == null ? null : data.second;
}
@Nullable
private static Map<String, Object> loadPubspecYamlInfo(final @NotNull String pubspecYamlFileContents) {
// see com.google.dart.tools.core.utilities.yaml.PubYamlUtils#parsePubspecYamlToMap() [https://github.com/dart-lang/eclipse3]
final Yaml yaml = new Yaml(new SafeConstructor(), new Representer(), new DumperOptions(), new Resolver() {
@Override
protected void addImplicitResolvers() {
addImplicitResolver(Tag.BOOL, BOOL, "yYnNtTfFoO");
addImplicitResolver(Tag.NULL, NULL, "~nN\0");
addImplicitResolver(Tag.NULL, EMPTY, null);
addImplicitResolver(new Tag(Tag.PREFIX + "value"), VALUE, "=");
addImplicitResolver(Tag.MERGE, MERGE, "<");
}
});
try {
//noinspection unchecked
return (Map<String, Object>)yaml.load(pubspecYamlFileContents);
}
catch (Exception e) {
return null; // malformed yaml, e.g. because of typing in it
}
}
}