blob: b4c84986e68f1fd80286ca525df7ee0a4fafc6de [file] [log] [blame]
/*
* 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);
}
}
}