]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5130 Show distribution of LOC and TechDebt by language
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 7 May 2014 13:05:05 +0000 (15:05 +0200)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 7 May 2014 13:05:05 +0000 (15:05 +0200)
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/SizeWidget.java
plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/size.html.erb
sonar-batch/src/main/java/org/sonar/batch/language/LanguageDistributionDecorator.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java
sonar-batch/src/test/java/org/sonar/batch/language/LanguageDistributionDecoratorTest.java [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties
sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java
sonar-plugin-api/src/test/java/org/sonar/api/resources/CoreMetricsTest.java
sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb

index b3330c7833f478734aa1d580d1160b5a0d4df651..0df494086711ca93d00a34b64a1ee9b25f314752 100644 (file)
@@ -22,6 +22,6 @@ package org.sonar.plugins.core.widgets;
 public class SizeWidget extends CoreWidget {
 
   public SizeWidget() {
-    super("size", "Size metrics", "/org/sonar/plugins/core/widgets/size.html.erb");
+    super("size", "Size metrics", "/Users/julienlancelot/Dev/Sources/sonar/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/size.html.erb");
   }
 }
index e3f741587f47a2f02041960043865b7918027e1d..f30117a5bde43c8d97d86855f393ae811d923e7b 100644 (file)
@@ -1,6 +1,7 @@
 <%
   lines=measure('lines')
   ncloc=measure('ncloc')
+  ncloc_language_distribution=measure('ncloc_language_distribution')
   classes=measure('classes')
   files=measure('files')
   functions=measure('functions')
@@ -8,6 +9,7 @@
   if measure('lines') || ncloc
     files=measure('files')
     statements=measure('statements')
+    languages = Api::Utils.java_facade.getLanguages()
 %>
 <table width="100%">
   <tr>
       <div class="dashbox">
 
         <% if ncloc %>
-          <h3><%= message('widget.size.lines_of_code') -%></h3>
+          <%
+             ncloc_language_dist_hash = Hash[*(ncloc_language_distribution.data.split(';').map { |elt| elt.split('=') }.flatten)] if ncloc_language_distribution
+             if ncloc_language_dist_hash && ncloc_language_dist_hash.size == 1
+               language_key = ncloc_language_dist_hash.first()[0].to_s
+               language = languages.find { |l| l.getKey()==language_key }
+          %>
+            <h3><%= message('widget.size.lines_of_code_with_language', :params => (language ? language.getName() : language_key)) -%></h3>
+          <% else %>
+            <h3><%= message('widget.size.lines_of_code') -%></h3>
+          <% end %>
           <p>
             <span class="big"><%= format_measure(ncloc, :suffix => '', :url => url_for_drilldown(ncloc)) -%></span>
             <%= dashboard_configuration.selected_period? ? format_variation(ncloc) : trend_icon(ncloc) -%>
         <% if projects %>
           <p><%= format_measure(projects, :suffix => message('widget.size.projects.suffix')) -%> <%= dashboard_configuration.selected_period? ? format_variation(projects) : trend_icon(projects) -%></p>
         <% end %>
+
+        <% if ncloc_language_dist_hash && ncloc_language_dist_hash.size > 1 %>
+          <table class="clear width100">
+            <%
+               max = ncloc_language_dist_hash.max_by{|_k,v| v.to_i}[1].to_i
+               # Sort lines language distribution by reverse number of lines
+               ncloc_language_dist_hash.sort {|v1,v2| v2[1].to_i <=> v1[1].to_i }.each do |language_key, language_ncloc|
+                tooltip = ncloc.format_numeric_value(language_ncloc) + message('widget.size.lines_of_code.suffix')
+            %>
+            <tr>
+              <td>
+                <% language = languages.find { |l| l.getKey()==language_key.to_s } -%>
+                <%= language ? language.getName() : language_key -%>
+              </td>
+              <td>&nbsp;</td>
+              <td align="left" style="padding-bottom:2px; padding-top:2px;">
+                <%= barchart(:width => 70, :percent => (100 * language_ncloc.to_i / max).to_i, :tooltip => tooltip)%>
+              </td>
+            </tr>
+            <% end %>
+          </table>
+        <% end %>
       </div>
     </td>
     <td width="10"> </td>
diff --git a/sonar-batch/src/main/java/org/sonar/batch/language/LanguageDistributionDecorator.java b/sonar-batch/src/main/java/org/sonar/batch/language/LanguageDistributionDecorator.java
new file mode 100644 (file)
index 0000000..ca77b7c
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.batch.language;
+
+import com.google.common.collect.ImmutableList;
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.DependedUpon;
+import org.sonar.api.batch.DependsUpon;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.CountDistributionBuilder;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.ResourceUtils;
+
+import java.util.List;
+
+public class LanguageDistributionDecorator implements Decorator {
+
+  public boolean shouldExecuteOnProject(Project project) {
+    return true;
+  }
+
+  @DependsUpon
+  public List<Metric> dependsUponMetrics() {
+    return ImmutableList.of(CoreMetrics.LINES);
+  }
+
+  @DependedUpon
+  public List<Metric> generatesMetrics() {
+    return ImmutableList.of(
+      CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION
+    );
+  }
+
+  public void decorate(Resource resource, DecoratorContext context) {
+    CountDistributionBuilder nclocDistribution = new CountDistributionBuilder(CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION);
+    if (ResourceUtils.isFile(resource)) {
+      Language language = resource.getLanguage();
+      Measure ncloc = context.getMeasure(CoreMetrics.NCLOC);
+      if (language != null && ncloc != null) {
+        nclocDistribution.add(language.getKey(), ncloc.getIntValue());
+      }
+    } else {
+      for (Measure measure : context.getChildrenMeasures(CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION)) {
+        nclocDistribution.add(measure);
+      }
+    }
+    Measure measure = nclocDistribution.build(false);
+    if (measure != null) {
+      context.saveMeasure(measure);
+    }
+  }
+
+}
index f4769d38316341d6a2d247ce4898ff37c02f726e..1db4a9a50541e8d4ec4416ab0b250f948121dea4 100644 (file)
@@ -46,6 +46,7 @@ import org.sonar.batch.issue.ignore.pattern.IssueExclusionPatternInitializer;
 import org.sonar.batch.issue.ignore.pattern.IssueInclusionPatternInitializer;
 import org.sonar.batch.issue.ignore.scanner.IssueExclusionsLoader;
 import org.sonar.batch.issue.ignore.scanner.IssueExclusionsRegexpScanner;
+import org.sonar.batch.language.LanguageDistributionDecorator;
 import org.sonar.batch.phases.PhaseExecutor;
 import org.sonar.batch.phases.PhasesTimeProfiler;
 import org.sonar.batch.qualitygate.GenerateQualityGateEvents;
@@ -150,6 +151,9 @@ public class ModuleScanContainer extends ComponentContainer {
       EnforceIssuesFilter.class,
       IgnoreIssuesFilter.class,
 
+      // language
+      LanguageDistributionDecorator.class,
+
       ScanPerspectives.class);
   }
 
diff --git a/sonar-batch/src/test/java/org/sonar/batch/language/LanguageDistributionDecoratorTest.java b/sonar-batch/src/test/java/org/sonar/batch/language/LanguageDistributionDecoratorTest.java
new file mode 100644 (file)
index 0000000..fadb7e8
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.batch.language;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.Scopes;
+import org.sonar.api.utils.KeyValueFormat;
+
+import java.util.Collections;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class LanguageDistributionDecoratorTest {
+
+  @Mock
+  DecoratorContext context;
+
+  @Mock
+  Resource resource;
+
+  @Captor
+  ArgumentCaptor<Measure> measureCaptor;
+
+  LanguageDistributionDecorator decorator;
+
+  @Before
+  public void setUp() throws Exception {
+    decorator = new LanguageDistributionDecorator();
+  }
+
+  @Test
+  public void depended_upon_metric() {
+    assertThat(decorator.generatesMetrics()).hasSize(1);
+  }
+
+  @Test
+  public void depens_upon_metric() {
+    assertThat(decorator.dependsUponMetrics()).hasSize(1);
+  }
+
+  @Test
+  public void save_ncloc_language_distribution_on_file() {
+    Language language = mock(Language.class);
+    when(language.getKey()).thenReturn("xoo");
+
+    when(resource.getScope()).thenReturn(Scopes.FILE);
+    when(resource.getLanguage()).thenReturn(language);
+    when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 200.0));
+
+    decorator.decorate(resource, context);
+
+    verify(context).saveMeasure(measureCaptor.capture());
+
+    Measure result = measureCaptor.getValue();
+    assertThat(result.getMetric()).isEqualTo(CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION);
+    assertThat(result.getData()).isEqualTo("xoo=200");
+  }
+
+  @Test
+  public void save_ncloc_language_distribution_on_project() {
+    when(resource.getScope()).thenReturn(Scopes.PROJECT);
+    when(context.getChildrenMeasures(CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION)).thenReturn(newArrayList(
+      new Measure(CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION, KeyValueFormat.format(ImmutableMap.of("java", 20))),
+      new Measure(CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION, KeyValueFormat.format(ImmutableMap.of("xoo", 150))),
+      new Measure(CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION, KeyValueFormat.format(ImmutableMap.of("xoo", 50)))
+    ));
+
+    decorator.decorate(resource, context);
+
+    verify(context).saveMeasure(measureCaptor.capture());
+
+    Measure result = measureCaptor.getValue();
+    assertThat(result.getMetric()).isEqualTo(CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION);
+    assertThat(result.getData()).isEqualTo("java=20;xoo=200");
+  }
+
+  @Test
+  public void not_save_language_distribution_on_file_if_no_measure() {
+    Language language = mock(Language.class);
+    when(language.getKey()).thenReturn("xoo");
+
+    when(resource.getScope()).thenReturn(Scopes.FILE);
+    when(resource.getLanguage()).thenReturn(language);
+    when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(null);
+
+    decorator.decorate(resource, context);
+
+    verify(context, never()).saveMeasure(measureCaptor.capture());
+  }
+
+  @Test
+  public void not_save_language_distribution_on_project_if_no_chidren_measures() {
+    when(resource.getScope()).thenReturn(Scopes.PROJECT);
+    when(context.getChildrenMeasures(CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION)).thenReturn(Collections.<Measure>emptyList());
+
+    decorator.decorate(resource, context);
+
+    verify(context, never()).saveMeasure(measureCaptor.capture());
+  }
+
+}
index ffc8bc14b22e3528c4e60b4557989358904c8074..c8ff38228ea10ff36670f005eefd1cc740544b37 100644 (file)
@@ -1077,6 +1077,8 @@ widget.rules.removed=Removed:
 widget.size.name=Size Metrics
 widget.size.description=Reports general metrics on the size of the project.
 widget.size.lines_of_code=Lines of code
+widget.size.lines_of_code_with_language=Lines of code ({0})
+widget.size.lines_of_code.suffix=\ lines of code
 widget.size.lines=Lines
 widget.size.generated.suffix=\ generated
 widget.size.lines.suffix=\ lines
index 852678a40af25b09ee8663289243bcff5594717a..1ac74b83a42e0ba64ffada0a03e2f274dfaaa428 100644 (file)
@@ -85,6 +85,21 @@ public final class CoreMetrics {
     .setFormula(new SumChildValuesFormula(false))
     .create();
 
+  /**
+   * @since 4.4
+   */
+  public static final String NCLOC_LANGUAGE_DISTRIBUTION_KEY = "ncloc_language_distribution";
+
+  /**
+   * @since 4.4
+   */
+  public static final Metric NCLOC_LANGUAGE_DISTRIBUTION = new Metric.Builder(NCLOC_LANGUAGE_DISTRIBUTION_KEY, "Lines of code per language", Metric.ValueType.DATA)
+    .setDescription("Non Commenting Lines of Code Distributed By Language")
+    .setDirection(Metric.DIRECTION_WORST)
+    .setQualitative(false)
+    .setDomain(DOMAIN_SIZE)
+    .create();
+
   public static final String GENERATED_NCLOC_KEY = "generated_ncloc";
   public static final Metric GENERATED_NCLOC = new Metric.Builder(GENERATED_NCLOC_KEY, "Generated lines of code", Metric.ValueType.INT)
     .setDescription("Generated non Commenting Lines of Code")
index 1c65ebfb51f92ef9863ed61e3a06c0a0aa5c2d75..086005be0f83aa8101c1bee2f8d094567a294645 100644 (file)
@@ -28,10 +28,12 @@ import java.util.List;
 import static org.fest.assertions.Assertions.assertThat;
 
 public class CoreMetricsTest {
+
   @Test
-  public void shouldReadMetricsFromClassReflection() {
+  public void read_metrics_from_class_reflection() {
     List<Metric> metrics = CoreMetrics.getMetrics();
-    assertThat(metrics).hasSize(151);
+    assertThat(metrics).hasSize(152);
     assertThat(metrics).contains(CoreMetrics.NCLOC, CoreMetrics.DIRECTORIES);
   }
+
 }
index 08227825e4b5bf2bbd64cb51ea8d0d4db5cbbd9f..4b1a24aa00da4dbd2f7d0cc1e9b51e0f25370f40 100644 (file)
@@ -391,7 +391,7 @@ module ApplicationHelper
     end
 
     align=(percent<0 ? 'float: right;' : nil)
-    "<div class='barchart' style='width: #{width}px'><div style='width: #{percent.abs}%;background-color:#{color};#{align}'></div></div>"
+    "<div class='barchart' style='width: #{width}px' title='#{options[:tooltip]}'><div style='width: #{percent.abs}%;background-color:#{color};#{align}'></div></div>"
   end
 
   def chart(parameters, options={})