/* * Copyright (c) 2007-2009, Osmorc Development Team * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this list * of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, this * list of conditions and the following disclaimer in the documentation and/or other * materials provided with the distribution. * * Neither the name of 'Osmorc Development Team' nor the names of its contributors may be * used to endorse or promote products derived from this software without specific * prior written permission. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.osmorc.inspection; import com.intellij.codeInsight.daemon.impl.analysis.AnnotationsHighlightUtil; import com.intellij.codeInspection.*; import com.intellij.codeInspection.ui.SingleCheckboxOptionsPanel; import com.intellij.ide.projectView.impl.ProjectRootsUtil; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; import com.intellij.packageDependencies.DependenciesBuilder; import com.intellij.packageDependencies.DependencyVisitorFactory; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiClassOwner; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.lang.manifest.psi.ManifestFile; import org.jetbrains.osgi.project.BundleManifest; import org.jetbrains.osgi.project.BundleManifestCache; import org.osgi.framework.Constants; import org.osmorc.facet.OsmorcFacet; import org.osmorc.util.OsgiPsiUtil; import javax.swing.*; import java.lang.annotation.RetentionPolicy; import java.util.List; import static org.osmorc.i18n.OsmorcBundle.message; /** * Inspection which checks if a package of a class is accessible inside the OSGi context. * * @author <a href="mailto:janthomae@janthomae.de">Jan Thomä</a> * @author <a href="mailto:robert@beeger.net">Robert F. Beeger</a> */ public class PackageAccessibilityInspection extends BaseJavaBatchLocalInspectionTool { public boolean checkTests = false; @Override public JComponent createOptionsPanel() { return new SingleCheckboxOptionsPanel(message("PackageAccessibilityInspection.ui.check.tests"), this, "checkTests"); } @Nullable @Override public ProblemDescriptor[] checkFile(@NotNull PsiFile file, @NotNull final InspectionManager manager, final boolean isOnTheFly) { if (!(file instanceof PsiClassOwner) || !checkTests && ProjectRootsUtil.isInTestSource(file)) { return null; } final OsmorcFacet facet = OsmorcFacet.getInstance(file); if (facet == null) { return null; } final List<ProblemDescriptor> problems = ContainerUtil.newSmartList(); DependenciesBuilder.analyzeFileDependencies(file, new DependenciesBuilder.DependencyProcessor() { @Override public void process(PsiElement place, PsiElement dependency) { if (dependency instanceof PsiClass) { Problem problem = checkAccessibility((PsiClass)dependency, facet); if (problem != null) { problems.add(manager.createProblemDescriptor(place, problem.message, isOnTheFly, problem.fixes, problem.type)); } } } }, DependencyVisitorFactory.VisitorOptions.SKIP_IMPORTS); return problems.isEmpty() ? null : problems.toArray(new ProblemDescriptor[problems.size()]); } private static class Problem { public final ProblemHighlightType type; public final String message; public final LocalQuickFix[] fixes; private Problem(ProblemHighlightType type, String message, LocalQuickFix... fixes) { this.type = type; this.message = message; this.fixes = fixes.length > 0 ? fixes : null; } public static Problem weak(String message, LocalQuickFix... fixes) { return new Problem(ProblemHighlightType.WEAK_WARNING, message, fixes); } public static Problem error(String message, LocalQuickFix... fixes) { return new Problem(ProblemHighlightType.GENERIC_ERROR_OR_WARNING, message, fixes); } } // OSGi Core Spec 3.5 "Class Loading Architecture" private static Problem checkAccessibility(PsiClass targetClass, OsmorcFacet facet) { // ignores annotations invisible at runtime if (targetClass.isAnnotationType()) { RetentionPolicy retention = AnnotationsHighlightUtil.getRetentionPolicy(targetClass); if (retention == RetentionPolicy.SOURCE || retention == RetentionPolicy.CLASS) { return null; } } // ignores files of unsupported type PsiFile targetFile = targetClass.getContainingFile(); if (!(targetFile instanceof PsiClassOwner)) { return null; } // accepts classes from the parent class loader (normally java.* packages from the boot class path) String packageName = ((PsiClassOwner)targetFile).getPackageName(); if (packageName.isEmpty() || packageName.startsWith("java.")) { return null; } // accepts classes from the bundle's class path (private packages) Module requestorModule = facet.getModule(); Module targetModule = ModuleUtilCore.findModuleForPsiElement(targetClass); if (targetModule == requestorModule) { return null; } BundleManifest importer = BundleManifestCache.getInstance(targetClass.getProject()).getManifest(requestorModule); if (importer != null && (importer.isPrivatePackage(packageName) || importer.getExportedPackage(packageName) != null)) { return null; } // rejects non-exported classes (manifest missing, or a package isn't listed as exported) BundleManifest exporter = BundleManifestCache.getInstance(targetClass.getProject()).getManifest(targetClass); if (exporter == null || exporter.getBundleSymbolicName() == null) { return Problem.weak(message("PackageAccessibilityInspection.non.osgi", packageName)); } String exportedPackage = exporter.getExportedPackage(packageName); if (exportedPackage == null) { return Problem.error(message("PackageAccessibilityInspection.not.exported", packageName)); } // ignores facets other than manually-edited manifests (most probably, they will have their import list correctly generated) if (!facet.getConfiguration().isManifestManuallyEdited()) { return null; } // accepts packages listed as imported or required if (importer != null) { if (importer.isPackageImported(packageName)) { return null; } if (importer.isBundleRequired(exporter.getBundleSymbolicName())) { return null; } // Attached fragments [AFAIK these should not be linked statically - r.sh] } return Problem.error(message("PackageAccessibilityInspection.not.imported", packageName), new ImportPackageFix(exportedPackage)); } private static class ImportPackageFix extends AbstractOsgiQuickFix { private final String myPackageToImport; public ImportPackageFix(String packageToImport) { myPackageToImport = packageToImport; } @NotNull @Override public String getName() { return message("PackageAccessibilityInspection.fix"); } @Override public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { ManifestFile manifestFile = getVerifiedManifestFile(descriptor.getPsiElement()); if (manifestFile != null) { new WriteCommandAction.Simple(project, manifestFile) { @Override protected void run() throws Throwable { OsgiPsiUtil.appendToHeader(manifestFile, Constants.IMPORT_PACKAGE, myPackageToImport); } }.execute(); } } } }