com.google.idea.blaze.android.rendering.BlazeRenderErrorContributor.java Source code

Java tutorial

Introduction

Here is the source code for com.google.idea.blaze.android.rendering.BlazeRenderErrorContributor.java

Source

/*
 * Copyright 2016 The Bazel Authors. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.idea.blaze.android.rendering;

import static com.android.SdkConstants.ANDROID_MANIFEST_XML;

import com.android.tools.idea.rendering.HtmlLinkManager;
import com.android.tools.idea.rendering.RenderErrorContributor;
import com.android.tools.idea.rendering.RenderLogger;
import com.android.tools.idea.rendering.RenderResult;
import com.android.tools.idea.rendering.errors.ui.RenderErrorModel;
import com.android.utils.HtmlBuilder;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.Maps;
import com.google.common.collect.SortedSetMultimap;
import com.google.common.collect.TreeMultimap;
import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
import com.google.idea.blaze.android.sync.model.AndroidResourceModuleRegistry;
import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
import com.google.idea.blaze.base.ideinfo.TargetKey;
import com.google.idea.blaze.base.ideinfo.TargetMap;
import com.google.idea.blaze.base.lang.buildfile.references.BuildReferenceManager;
import com.google.idea.blaze.base.model.BlazeProjectData;
import com.google.idea.blaze.base.settings.Blaze;
import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
import com.google.idea.blaze.base.targetmaps.SourceToTargetMap;
import com.google.idea.blaze.base.targetmaps.TransitiveDependencyMap;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.search.GlobalSearchScope;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.SortedMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;

/** Contribute blaze specific render errors. */
public class BlazeRenderErrorContributor extends RenderErrorContributor {
    private RenderLogger logger;
    private Module module;
    private Project project;

    public BlazeRenderErrorContributor(RenderResult result, @Nullable DataContext dataContext) {
        super(result, dataContext);
        logger = result.getLogger();
        module = result.getModule();
        project = module.getProject();
    }

    @Override
    public Collection<RenderErrorModel.Issue> reportIssues() {
        BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();

        if (blazeProjectData == null || !logger.hasErrors()) {
            return getIssues();
        }

        TargetMap targetMap = blazeProjectData.targetMap;
        ArtifactLocationDecoder decoder = blazeProjectData.artifactLocationDecoder;
        AndroidResourceModule resourceModule = AndroidResourceModuleRegistry.getInstance(project).get(module);
        if (resourceModule == null) {
            return getIssues();
        }

        TargetIdeInfo target = targetMap.get(resourceModule.targetKey);
        if (target == null) {
            return getIssues();
        }

        reportGeneratedResources(resourceModule, targetMap, decoder);
        reportNonStandardAndroidManifestName(target, decoder);
        reportResourceTargetShouldDependOnClassTarget(target, targetMap, decoder);
        return getIssues();
    }

    /**
     * We can't find generated resources. If a layout uses them, the layout won't render correctly.
     */
    private void reportGeneratedResources(AndroidResourceModule resourceModule, TargetMap targetMap,
            ArtifactLocationDecoder decoder) {
        Map<String, Throwable> brokenClasses = logger.getBrokenClasses();
        if (brokenClasses == null || brokenClasses.isEmpty()) {
            return;
        }

        // Sorted entries for deterministic error message.
        SortedMap<ArtifactLocation, TargetIdeInfo> generatedResources = Maps
                .newTreeMap(getGeneratedResources(targetMap.get(resourceModule.targetKey)));

        for (TargetKey dependency : resourceModule.transitiveResourceDependencies) {
            generatedResources.putAll(getGeneratedResources(targetMap.get(dependency)));
        }

        if (generatedResources.isEmpty()) {
            return;
        }

        HtmlBuilder builder = new HtmlBuilder();
        builder.add("Generated resources will not be discovered by the IDE:");
        builder.beginList();
        for (Map.Entry<ArtifactLocation, TargetIdeInfo> entry : generatedResources.entrySet()) {
            ArtifactLocation resource = entry.getKey();
            TargetIdeInfo target = entry.getValue();
            builder.listItem().add(resource.getRelativePath()).add(" from ");
            addTargetLink(builder, target, decoder);
        }
        builder.endList().add("Please avoid using generated resources, ")
                .addLink("then ", "sync the project", " ", getLinkManager().createSyncProjectUrl())
                .addLink("and ", "refresh the layout", ".", getLinkManager().createRefreshRenderUrl());
        addIssue().setSeverity(HighlightSeverity.ERROR, HIGH_PRIORITY + 1) // Reported above broken classes
                .setSummary("Generated resources").setHtmlContent(builder).build();
    }

    private static SortedMap<ArtifactLocation, TargetIdeInfo> getGeneratedResources(TargetIdeInfo target) {
        if (target == null || target.androidIdeInfo == null) {
            return Collections.emptySortedMap();
        }
        SortedMap<ArtifactLocation, TargetIdeInfo> generatedResources = Maps.newTreeMap();
        generatedResources.putAll(target.androidIdeInfo.resources.stream().filter(ArtifactLocation::isGenerated)
                .collect(Collectors.toMap(Function.identity(), resource -> target)));
        return generatedResources;
    }

    /**
     * When the Android manifest isn't AndroidManifest.xml, resolving resource IDs would fail. This
     * doesn't seem to be an issue if the manifest belongs to one of the target's dependencies.
     */
    private void reportNonStandardAndroidManifestName(TargetIdeInfo target, ArtifactLocationDecoder decoder) {
        if (target.androidIdeInfo == null || target.androidIdeInfo.manifest == null) {
            return;
        }

        Map<String, Throwable> brokenClasses = logger.getBrokenClasses();
        if (brokenClasses == null || brokenClasses.isEmpty()) {
            return;
        }

        File manifest = decoder.decode(target.androidIdeInfo.manifest);
        if (manifest.getName().equals(ANDROID_MANIFEST_XML)) {
            return;
        }

        HtmlBuilder builder = new HtmlBuilder();
        addTargetLink(builder, target, decoder).add(" uses a non-standard name for the Android manifest: ");
        String linkToManifest = HtmlLinkManager.createFilePositionUrl(manifest, -1, 0);
        if (linkToManifest != null) {
            builder.addLink(manifest.getName(), linkToManifest);
        } else {
            builder.newline().add(manifest.getPath());
        }
        // TODO: add a link to automatically rename the file and refactor all references.
        builder.newline().add("Please rename it to ").add(ANDROID_MANIFEST_XML)
                .addLink(", then ", "sync the project", "", getLinkManager().createSyncProjectUrl())
                .addLink(" and ", "refresh the layout", ".", getLinkManager().createRefreshRenderUrl());
        addIssue().setSeverity(HighlightSeverity.ERROR, HIGH_PRIORITY + 1) // Reported above broken classes.
                .setSummary("Non-standard manifest name").setHtmlContent(builder).build();
    }

    /**
     * Blaze doesn't resolve class dependencies from resources until building the final
     * android_binary, so we could end up with resources that ultimately build correctly, but fail to
     * find their class dependencies during rendering in the layout editor.
     */
    private void reportResourceTargetShouldDependOnClassTarget(TargetIdeInfo target, TargetMap targetMap,
            ArtifactLocationDecoder decoder) {
        Collection<String> missingClasses = logger.getMissingClasses();
        if (missingClasses == null || missingClasses.isEmpty()) {
            return;
        }

        // Sorted entries for deterministic error message.
        SortedSetMultimap<String, TargetKey> missingClassToTargetMap = TreeMultimap.create();

        SourceToTargetMap sourceToTargetMap = SourceToTargetMap.getInstance(project);
        ImmutableCollection transitiveDependencies = TransitiveDependencyMap.getInstance(project)
                .getTransitiveDependencies(target.key);

        for (String missingClass : missingClasses) {
            File sourceFile = getSourceFileForClass(missingClass);
            if (sourceFile == null) {
                continue;
            }
            ImmutableCollection<TargetKey> sourceTargets = sourceToTargetMap.getRulesForSourceFile(sourceFile);
            if (sourceTargets.stream().noneMatch(sourceTarget -> sourceTarget.equals(target.key)
                    || transitiveDependencies.contains(sourceTarget))) {
                missingClassToTargetMap.putAll(missingClass, sourceTargets);
            }
        }

        if (missingClassToTargetMap.isEmpty()) {
            return;
        }

        HtmlBuilder builder = new HtmlBuilder();
        addTargetLink(builder, target, decoder).add(" contains resource files that reference these classes:")
                .beginList();
        for (String missingClass : missingClassToTargetMap.keySet()) {
            builder.listItem().addLink(missingClass, getLinkManager().createOpenClassUrl(missingClass))
                    .add(" from ");
            for (TargetKey targetKey : missingClassToTargetMap.get(missingClass)) {
                addTargetLink(builder, targetMap.get(targetKey), decoder).add(" ");
            }
        }
        builder.endList().add("Please fix your dependencies so that ");
        addTargetLink(builder, target, decoder).add(" correctly depends on these classes, ")
                .addLink("then ", "sync the project", " ", getLinkManager().createSyncProjectUrl())
                .addLink("and ", "refresh the layout", ".", getLinkManager().createRefreshRenderUrl()).newline()
                .newline()
                .addBold("NOTE: blaze can still build with the incorrect dependencies "
                        + "due to the way it handles resources, "
                        + "but the layout editor needs them to be correct.");

        addIssue().setSeverity(HighlightSeverity.ERROR, HIGH_PRIORITY + 1) // Reported above missing classes.
                .setSummary("Missing class dependencies").setHtmlContent(builder).build();
    }

    private File getSourceFileForClass(String className) {
        return ApplicationManager.getApplication().runReadAction((Computable<File>) () -> {
            try {
                PsiClass psiClass = JavaPsiFacade.getInstance(project).findClass(className,
                        GlobalSearchScope.projectScope(project));
                if (psiClass == null) {
                    return null;
                }
                return VfsUtilCore.virtualToIoFile(psiClass.getContainingFile().getVirtualFile());
            } catch (IndexNotReadyException ignored) {
                // We're in dumb mode. Abort! Abort!
                return null;
            }
        });
    }

    private HtmlBuilder addTargetLink(HtmlBuilder builder, TargetIdeInfo target, ArtifactLocationDecoder decoder) {
        File buildFile = decoder.decode(target.buildFile);
        int line = ApplicationManager.getApplication().runReadAction((Computable<Integer>) () -> {
            PsiElement buildTargetPsi = BuildReferenceManager.getInstance(project).resolveLabel(target.key.label);
            if (buildTargetPsi == null) {
                return -1;
            }
            return StringUtil.offsetToLineNumber(buildTargetPsi.getContainingFile().getText(),
                    buildTargetPsi.getTextOffset());
        });
        String url = HtmlLinkManager.createFilePositionUrl(buildFile, line, 0);
        if (url != null) {
            return builder.addLink(target.toString(), url);
        }
        return builder.add(target.toString());
    }

    /** Extension to provide {@link BlazeRenderErrorContributor}. */
    public static class BlazeProvider extends Provider {
        @Override
        public boolean isApplicable(Project project) {
            return Blaze.isBlazeProject(project);
        }

        @Override
        public RenderErrorContributor getContributor(RenderResult result, @Nullable DataContext dataContext) {
            return new BlazeRenderErrorContributor(result, dataContext);
        }
    }
}