/* * Sonar SonarJ Plugin * Copyright (C) 2009, 2010 hello2morrow GmbH * mailto: info AT hello2morrow DOT com * * 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.hello2morrow.sonarplugin; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import com.hello2morrow.sonarplugin.xsd.XsdBuildUnits; import org.apache.commons.configuration.Configuration; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.Sensor; import org.sonar.api.batch.SensorContext; import org.sonar.api.batch.maven.DependsUponMavenPlugin; import org.sonar.api.batch.maven.MavenPluginHandler; import org.sonar.api.measures.Measure; import org.sonar.api.measures.Metric; import org.sonar.api.profiles.RulesProfile; import org.sonar.api.resources.JavaFile; import org.sonar.api.resources.JavaPackage; import org.sonar.api.resources.Project; import org.sonar.api.resources.Resource; import org.sonar.api.rules.ActiveRule; import org.sonar.api.rules.Rule; import org.sonar.api.rules.RulePriority; import org.sonar.api.rules.RulesManager; import org.sonar.api.rules.Violation; import com.hello2morrow.sonarplugin.xsd.ReportContext; import com.hello2morrow.sonarplugin.xsd.XsdArchitectureViolation; import com.hello2morrow.sonarplugin.xsd.XsdAttribute; import com.hello2morrow.sonarplugin.xsd.XsdAttributeCategory; import com.hello2morrow.sonarplugin.xsd.XsdAttributeRoot; import com.hello2morrow.sonarplugin.xsd.XsdCycleGroup; import com.hello2morrow.sonarplugin.xsd.XsdCycleGroups; import com.hello2morrow.sonarplugin.xsd.XsdCyclePath; import com.hello2morrow.sonarplugin.xsd.XsdDependencyProblem; import com.hello2morrow.sonarplugin.xsd.XsdElementProblem; import com.hello2morrow.sonarplugin.xsd.XsdPosition; import com.hello2morrow.sonarplugin.xsd.XsdProblemCategory; import com.hello2morrow.sonarplugin.xsd.XsdProjects; import com.hello2morrow.sonarplugin.xsd.XsdTask; import com.hello2morrow.sonarplugin.xsd.XsdTasks; import com.hello2morrow.sonarplugin.xsd.XsdTypeRelation; import com.hello2morrow.sonarplugin.xsd.XsdViolations; import com.hello2morrow.sonarplugin.xsd.XsdWarning; import com.hello2morrow.sonarplugin.xsd.XsdWarnings; import com.hello2morrow.sonarplugin.xsd.XsdWarningsByAttribute; import com.hello2morrow.sonarplugin.xsd.XsdWarningsByAttributeGroup; public final class SonarJSensor implements Sensor { public static final String COST_PER_INDEX_POINT = "sonarj.index_point_cost"; private static final Logger LOG = LoggerFactory.getLogger(SonarJSensor.class); private static final String REPORT_DIR = "sonarj-sonar-plugin"; private static final String REPORT_NAME = "sonarj-report.xml"; private static final String ACD = "Average component dependency (ACD)"; private static final String NCCD = "Normalized cumulative component dependency (NCCD)"; private static final String INTERNAL_PACKAGES = "Number of internal packages"; private static final String INSTRUCTIONS = "Number of instructions"; private static final String UNASSIGNED_TYPES = "Number of unassigned types"; private static final String VIOLATING_DEPENDENCIES = "Number of violating type dependencies"; private static final String VIOLATING_TYPES = "Number of violating types"; private static final String TYPE_DEPENDENCIES = "Number of type dependencies (all)"; private static final String JAVA_FILES = "Number of Java source files (non-excluded)"; private static final String IGNORED_VIOLATIONS = "Number of ignored violations"; private static final String IGNORED_WARNINGS = "Number of ignored warnings"; private static final String TASKS = "Number of tasks"; private static final String ALL_WARNINGS = "Number of warnings (all)"; private static final String CYCLE_WARNINGS = "Number of warnings (cyclic)"; private static final String THRESHOLD_WARNINGS = "Number of warnings (thresholds)"; private static final String WORKSPACE_WARNINGS = "Number of warnings (workspace)"; private static final String DUPLICATE_WARNINGS = "Number of warnings (duplicate code blocks)"; private static final String EROSION_REFS = "Structural erosion - reference level"; private static final String EROSION_TYPES = "Structural erosion - type level"; private static final String INTERNAL_TYPES = "Number of internal types (all)"; private static final String STUCTURAL_DEBT_INDEX = "Structural debt (index)"; private Map<String, Number> projectMetrics; private SensorContext sensorContext; private RulesManager rulesManager; private RulesProfile rulesProfile; private double indexCost = 12.0; protected static ReportContext readSonarjReport(String fileName, String packaging) { ReportContext result = null; InputStream input = null; ClassLoader defaultClassLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(SonarJSensor.class.getClassLoader()); JAXBContext context = JAXBContext.newInstance("com.hello2morrow.sonarplugin.xsd"); Unmarshaller u = context.createUnmarshaller(); input = new FileInputStream(fileName); result = (ReportContext) u.unmarshal(input); } catch (JAXBException e) { LOG.error("JAXB Problem in " + fileName, e); } catch (FileNotFoundException e) { if (!packaging.equalsIgnoreCase("pom")) { LOG.warn("Cannot open SonarJ report: " + fileName + "."); LOG.warn(" Did you run the maven sonarj goal before with the POM option <prepareForSonar>true</prepareForSonar> " + "or with the commandline option -Dsonarj.prepareForSonar=true?"); LOG.warn(" Is the project part of the SonarJ architecture description?"); LOG.warn(" Did you set the 'aggregate' to true (must be false)?"); } } finally { Thread.currentThread().setContextClassLoader(defaultClassLoader); if (input != null) { try { input.close(); } catch (IOException e) { LOG.error("Cannot close " + fileName, e); } } } return result; } public SonarJSensor(Configuration config, RulesManager rulesManager, RulesProfile rulesProfile) { indexCost = config.getDouble(COST_PER_INDEX_POINT, 12.0); this.rulesManager = rulesManager; this.rulesProfile = rulesProfile; if (rulesManager == null) { LOG.warn("No RulesManager provided to sensor"); } if (rulesProfile == null) { LOG.warn("No RulesProfile given to sensor"); } } public boolean shouldExecuteOnProject(Project project) { return true; } private Map<String, Number> readAttributes(XsdAttributeRoot root) { Map<String, Number> result = new HashMap<String, Number>(); for (XsdAttributeCategory cat : root.getAttributeCategory()) { for (XsdAttribute attr : cat.getAttribute()) { String attrName = attr.getName(); String value = attr.getValue(); try { if (value.indexOf('.') >= 0) { result.put(attrName, Double.valueOf(value)); } else if (value.indexOf(':') == -1) { result.put(attrName, Integer.valueOf(value)); } } catch (NumberFormatException e) { // Ignore this value } } } return result; } private double getProjectMetric(String key) { Number num = projectMetrics.get(key); if (num == null) { LOG.error("Cannot find metric <" + key + "> in generated report"); return 0.0; } return num.doubleValue(); } private Measure saveMeasure(String key, Metric metric, int precision) { double value = getProjectMetric(key); return saveMeasure(metric, value, precision); } private Measure saveMeasure(Metric metric, double value, int precision) { Measure m = new Measure(metric, value, precision); sensorContext.saveMeasure(m); return m; } private String getAttribute(List<XsdAttribute> map, String name) { String value = null; for (XsdAttribute attr : map) { if (attr.getName().equals(name)) { value = attr.getValue(); break; } } return value; } @SuppressWarnings("unchecked") private void analyseCycleGroups(ReportContext report, Number internalPackages, String buildUnitName) { XsdCycleGroups cycleGroups = report.getCycleGroups(); double cyclicity = 0; double biggestCycleGroupSize = 0; double cyclicPackages = 0; for (XsdCycleGroup group : cycleGroups.getCycleGroup()) { if (group.getNamedElementGroup().equals("Physical build unit package")) { if (getBuildUnitName(group).equals(buildUnitName)) { int groupSize = group.getCyclePath().size(); cyclicPackages += groupSize; cyclicity += groupSize * groupSize; if (groupSize > biggestCycleGroupSize) { biggestCycleGroupSize = groupSize; } handlePackageCycleGroup(group); } } } saveMeasure(SonarJMetrics.BIGGEST_CYCLE_GROUP, biggestCycleGroupSize, 0); saveMeasure(SonarJMetrics.CYCLICITY, cyclicity, 0); saveMeasure(SonarJMetrics.CYCLIC_PACKAGES, cyclicPackages, 0); double relativeCyclicity = 100.0 * Math.sqrt(cyclicity) / internalPackages.doubleValue(); double relativeCyclicPackages = 100.0 * cyclicPackages / internalPackages.doubleValue(); saveMeasure(SonarJMetrics.RELATIVE_CYCLICITY, relativeCyclicity, 1); saveMeasure(SonarJMetrics.INTERNAL_PACKAGES, internalPackages.doubleValue(), 0); saveMeasure(SonarJMetrics.CYCLIC_PACKAGES_PERCENT, relativeCyclicPackages, 1); } @SuppressWarnings("unchecked") private void saveViolation(Rule rule, RulePriority priority, String fqName, int line, String msg) { Resource javaFile = sensorContext.getResource(new JavaFile(fqName)); if (javaFile == null) { LOG.error("Cannot obtain resource " + fqName); } else { Violation v = new Violation(rule, javaFile); v.setMessage(msg); v.setLineId(line); v.setPriority(priority); sensorContext.saveViolation(v); } } private String getBuildUnitName(XsdCycleGroup group) { if (group.getParent().equals("(Default Build Unit)")) { return group.getElementScope(); } return group.getParent(); } private String getBuildUnitName(String fqName) { String buName = "<UNKNOWN>"; if (fqName != null) { int colonPos = fqName.indexOf("::"); if (colonPos != -1) { buName = fqName.substring(colonPos + 2); if (buName.equals("(Default Build Unit)")) { // Compatibility with old SonarJ versions buName = fqName.substring(0, colonPos); } } } return buName; } private static String relativeFileNameToFqName(String fileName) { int lastDot = fileName.lastIndexOf('.'); return fileName.substring(0, lastDot).replace('/', '.'); } private void handlePackageCycleGroup(XsdCycleGroup group) { Rule rule = rulesManager.getPluginRule(SonarJPluginBase.PLUGIN_KEY, SonarJPluginBase.CYCLE_GROUP_RULE_KEY); ActiveRule activeRule = rulesProfile.getActiveRule(SonarJPluginBase.PLUGIN_KEY, SonarJPluginBase.CYCLE_GROUP_RULE_KEY); if (rule != null && activeRule != null) { for (XsdCyclePath pathElement : group.getCyclePath()) { String fqName = pathElement.getParent(); Resource javaPackage = sensorContext.getResource(new JavaPackage(fqName)); if (javaPackage == null) { LOG.error("Cannot obtain resource " + fqName); } else { Violation v = new Violation(rule, javaPackage); v.setMessage("Package participates in a cycle group"); v.setLineId(1); v.setPriority(activeRule.getPriority()); sensorContext.saveViolation(v); } } } } private int handleArchitectureViolations(XsdViolations violations, String buildUnitName) { Rule rule = rulesManager.getPluginRule(SonarJPluginBase.PLUGIN_KEY, SonarJPluginBase.ARCH_RULE_KEY); ActiveRule activeRule = rulesProfile.getActiveRule(SonarJPluginBase.PLUGIN_KEY, SonarJPluginBase.ARCH_RULE_KEY); int count = 0; for (XsdArchitectureViolation violation : violations.getArchitectureViolations()) { String toName = getAttribute(violation.getArchitectureViolation().getAttribute(), "To"); String toElemType = getAttribute(violation.getArchitectureViolation().getAttribute(), "To element type").toLowerCase(); String target = toElemType + ' ' + toName; for (XsdTypeRelation rel : violation.getTypeRelation()) { String toType = getAttribute(rel.getAttribute(), "To"); String msg = "Type " + toType + " from " + target + " must not be used from here"; String bu = getAttribute(rel.getAttribute(), "From build unit"); bu = getBuildUnitName(bu); if (bu.equals(buildUnitName)) { for (XsdPosition pos : rel.getPosition()) { if (rule != null && activeRule != null) { String relFileName = pos.getFile(); if (relFileName != null) { String fqName = relativeFileNameToFqName(relFileName); saveViolation(rule, activeRule.getPriority(), fqName, Integer.valueOf(pos.getLine()), msg); } } count++; } } } } if (rule == null) { LOG.error("SonarJ architecture rule not found"); } else if (activeRule == null) { LOG.warn("SonarJ architecture rule deactivated"); } return count; } private String getRuleKey(String attributeGroup) { if (attributeGroup.equals("Duplicate code")) { return SonarJPluginBase.DUPLICATE_RULE_KEY; } if (attributeGroup.equals("Workspace")) { return SonarJPluginBase.WORKSPACE_RULE_KEY; } if (attributeGroup.equals("Threshold")) { return SonarJPluginBase.THRESHOLD_RULE_KEY; } return null; } private void handleWarnings(XsdWarnings warnings, String buildUnitName) { for (XsdWarningsByAttributeGroup warningGroup : warnings.getWarningsByAttributeGroup()) { String key = getRuleKey(warningGroup.getAttributeGroup()); if (key == null) { continue; } Rule rule = rulesManager.getPluginRule(SonarJPluginBase.PLUGIN_KEY, key); ActiveRule activeRule = rulesProfile.getActiveRule(SonarJPluginBase.PLUGIN_KEY, key); if (rule == null) { LOG.error("SonarJ threshold rule not found"); continue; } if (activeRule == null) { LOG.info("SonarJ threshold rule deactivated"); continue; } for (XsdWarningsByAttribute warningByAttribute : warningGroup.getWarningsByAttribute()) { String attrName = warningByAttribute.getAttributeName(); for (XsdWarning warning : warningByAttribute.getWarning()) { String msg = attrName + "=" + getAttribute(warning.getAttribute(), "Attribute value"); String bu = getAttribute(warning.getAttribute(), "Build unit"); bu = getBuildUnitName(bu); if (bu.equals(buildUnitName)) { if (warning.getPosition().size() > 0) { for (XsdPosition pos : warning.getPosition()) { String relFileName = pos.getFile(); if (relFileName != null) { String fqName = relativeFileNameToFqName(relFileName); saveViolation(rule, activeRule.getPriority(), fqName, Integer.valueOf(pos.getLine()), msg); } } } else { String elemType = getAttribute(warning.getAttribute(), "Element type"); if (elemType.equals("Class file") || elemType.equals("Source file")) { // Attach a violation at line 1 String fileName = getAttribute(warning.getAttribute(), "Element"); String fqName = fileName.substring(0, fileName.lastIndexOf('.')).replace('/', '.'); saveViolation(rule, activeRule.getPriority(), fqName, 1, msg); } } } } } } } private String handleDescription(String descr) { if (descr.startsWith("Fix warning")) { // TODO: handle ascending metrics correctly (99% are descending) return "Reduce" + descr.substring(descr.indexOf(':') + 1).toLowerCase(); } if (descr.startsWith("Cut type")) { String toType = descr.substring(descr.indexOf("to ")); return "Cut dependency " + toType; } if (descr.startsWith("Move type")) { String to = descr.substring(descr.indexOf("to ")); return "Move " + to; } return descr; } private int handleTasks(XsdTasks tasks, String buildUnitName) { Map<String, RulePriority> priorityMap = new HashMap<String, RulePriority>(); Rule rule = rulesManager.getPluginRule(SonarJPluginBase.PLUGIN_KEY, SonarJPluginBase.TASK_RULE_KEY); int count = 0; if (rule == null) { LOG.error("SonarJ task rule not found"); return 0; } ActiveRule activeRule = rulesProfile.getActiveRule(SonarJPluginBase.PLUGIN_KEY, SonarJPluginBase.TASK_RULE_KEY); if (activeRule == null) { LOG.info("SonarJ task rule not activated"); } priorityMap.put("Low", RulePriority.INFO); priorityMap.put("Medium", RulePriority.MINOR); priorityMap.put("High", RulePriority.MAJOR); for (XsdTask task : tasks.getTask()) { String bu = getAttribute(task.getAttribute(), "Build unit"); bu = getBuildUnitName(bu); if (bu.equals(buildUnitName)) { String priority = getAttribute(task.getAttribute(), "Priority"); String description = getAttribute(task.getAttribute(), "Description"); String assignedTo = getAttribute(task.getAttribute(), "Assigned to"); description = handleDescription(description); // This should not be needed, but the current description sucks int index = description.indexOf(" package"); if (index > 0 && index < 8) { // Package refactorings won't get markers - this would create to many non relevant markers count++; } else { if (assignedTo != null) { assignedTo = '[' + StringUtils.trim(assignedTo) + ']'; if (assignedTo.length() > 2) { description += ' ' + assignedTo; } } for (XsdPosition pos : task.getPosition()) { String relFileName = pos.getFile(); if (relFileName != null) { String fqName = relativeFileNameToFqName(relFileName); int line = Integer.valueOf(pos.getLine()); if (line == 0) { line = 1; } if (activeRule != null) { saveViolation(rule, priorityMap.get(priority), fqName, line, description); } } count++; } } } } return count; } private void addArchitectureMeasures(ReportContext report, String buildUnitName) { double types = saveMeasure(INTERNAL_TYPES, SonarJMetrics.INTERNAL_TYPES, 0).getValue(); Measure unassignedTypes = saveMeasure(UNASSIGNED_TYPES, SonarJMetrics.UNASSIGNED_TYPES, 0); Measure violatingTypes = saveMeasure(VIOLATING_TYPES, SonarJMetrics.VIOLATING_TYPES, 0); saveMeasure(VIOLATING_DEPENDENCIES, SonarJMetrics.VIOLATING_DEPENDENCIES, 0); saveMeasure(TASKS, SonarJMetrics.TASKS, 0); saveMeasure(THRESHOLD_WARNINGS, SonarJMetrics.THRESHOLD_WARNINGS, 0); saveMeasure(WORKSPACE_WARNINGS, SonarJMetrics.WORKSPACE_WARNINGS, 0); saveMeasure(IGNORED_VIOLATIONS, SonarJMetrics.IGNORED_VIOLATONS, 0); saveMeasure(IGNORED_WARNINGS, SonarJMetrics.IGNORED_WARNINGS, 0); saveMeasure(DUPLICATE_WARNINGS, SonarJMetrics.DUPLICATE_WARNINGS, 0); saveMeasure(CYCLE_WARNINGS, SonarJMetrics.CYCLE_WARNINGS, 0); saveMeasure(ALL_WARNINGS, SonarJMetrics.ALL_WARNINGS, 0); assert types >= 1.0 : "Project must not be empty !"; double violatingTypesPercent = 100.0 * violatingTypes.getValue() / types; double unassignedTypesPercent = 100.0 * unassignedTypes.getValue() / types; saveMeasure(SonarJMetrics.VIOLATING_TYPES_PERCENT, violatingTypesPercent, 1); saveMeasure(SonarJMetrics.UNASSIGNED_TYPES_PERCENT, unassignedTypesPercent, 1); XsdViolations violations = report.getViolations(); double violating_refs = 0; double task_refs = 0; if (rulesManager != null && rulesProfile != null) { violating_refs = handleArchitectureViolations(violations, buildUnitName); handleWarnings(report.getWarnings(), buildUnitName); task_refs = handleTasks(report.getTasks(), buildUnitName); } saveMeasure(SonarJMetrics.ARCHITECTURE_VIOLATIONS, violating_refs, 0); saveMeasure(SonarJMetrics.TASK_REFS, task_refs, 0); } private void analyse(IProject project, XsdAttributeRoot xsdBuildUnit, String buildUnitName, ReportContext report, boolean isMultiModuleProject) { LOG.info("Adding measures for " + project.getName()); projectMetrics = readAttributes(xsdBuildUnit); Number internalPackages = projectMetrics.get(INTERNAL_PACKAGES); if (internalPackages.intValue() == 0) { LOG.warn("No classes found in project " + project.getName()); return; } double acd = projectMetrics.get(ACD).doubleValue(); double nccd = projectMetrics.get(NCCD).doubleValue(); saveMeasure(ACD, SonarJMetrics.ACD, 1); saveMeasure(NCCD, SonarJMetrics.NCCD, 1); saveMeasure(INSTRUCTIONS, SonarJMetrics.INSTRUCTIONS, 0); saveMeasure(JAVA_FILES, SonarJMetrics.JAVA_FILES, 0); saveMeasure(TYPE_DEPENDENCIES, SonarJMetrics.TYPE_DEPENDENCIES, 0); saveMeasure(EROSION_REFS, SonarJMetrics.EROSION_REFS, 0); saveMeasure(EROSION_TYPES, SonarJMetrics.EROSION_TYPES, 0); Number structuralDebtIndex = saveMeasure(STUCTURAL_DEBT_INDEX, SonarJMetrics.EROSION_INDEX, 0).getValue(); if (indexCost > 0) { double structuralDebtCost = structuralDebtIndex.doubleValue() * indexCost; saveMeasure(SonarJMetrics.EROSION_COST, structuralDebtCost, 0); } analyseCycleGroups(report, internalPackages, buildUnitName); if (projectMetrics.get(UNASSIGNED_TYPES) != null) { LOG.info("Adding architecture measures for " + project.getName()); addArchitectureMeasures(report, buildUnitName); } AlertDecorator.setAlertLevels(new SensorProjectContext(sensorContext)); } protected void analyse(IProject project, SensorContext sensorContext, ReportContext report) { this.sensorContext = sensorContext; XsdBuildUnits buildUnits = report.getBuildUnits(); List<XsdAttributeRoot> buildUnitList = buildUnits.getBuildUnit(); if (buildUnitList.size() > 1) { String longName = project.getArtifactId() + "[" + project.getGroupId() + "]"; String longName2 = project.getGroupId() + ':' + project.getArtifactId(); for (XsdAttributeRoot sonarBuildUnit : buildUnitList) { String buName = sonarBuildUnit.getName(); buName = getBuildUnitName(buName); if (buName.equals(project.getArtifactId()) || buName.equals(longName) || buName.equals(longName2)) { analyse(project, sonarBuildUnit, buName, report, true); break; } else { if (buName.startsWith("...") && longName2.endsWith(buName.substring(2))) { analyse(project, sonarBuildUnit, buName, report, true); break; } } } } else { String buName = buildUnitList.get(0).getName(); buName = getBuildUnitName(buName); analyse(project, buildUnitList.get(0), buName, report, false); } } public void analyse(Project project, SensorContext sensorContext) { LOG.info("------------------------------------------------------------------------"); LOG.info("Execute sonar-sonarj-plugin for " + project.getName()); LOG.info("------------------------------------------------------------------------"); ReportContext report = readSonarjReport(getReportFileName(project), project.getPackaging()); if (report != null) { analyse(new ProjectDelegate(project), sensorContext, report); } } public final String getReportFileName(Project project) { return project.getFileSystem().getBuildDir().getPath() + '/' + REPORT_DIR + '/' + REPORT_NAME; } }