From: Julien Lancelot Date: Wed, 7 May 2014 13:05:05 +0000 (+0200) Subject: SONAR-5130 Show distribution of LOC and TechDebt by language X-Git-Tag: 4.4-RC1~1175 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=8f936044deed48e659bffaa9e6587fe22143a192;p=sonarqube.git SONAR-5130 Show distribution of LOC and TechDebt by language --- diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/SizeWidget.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/SizeWidget.java index b3330c7833f..0df49408671 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/SizeWidget.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/SizeWidget.java @@ -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"); } } diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/size.html.erb b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/size.html.erb index e3f741587f4..f30117a5bde 100644 --- a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/size.html.erb +++ b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/size.html.erb @@ -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() %> @@ -15,7 +17,16 @@
<% if ncloc %> -

<%= message('widget.size.lines_of_code') -%>

+ <% + 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 } + %> +

<%= message('widget.size.lines_of_code_with_language', :params => (language ? language.getName() : language_key)) -%>

+ <% else %> +

<%= message('widget.size.lines_of_code') -%>

+ <% end %>

<%= format_measure(ncloc, :suffix => '', :url => url_for_drilldown(ncloc)) -%> <%= dashboard_configuration.selected_period? ? format_variation(ncloc) : trend_icon(ncloc) -%> @@ -45,6 +56,28 @@ <% if projects %>

<%= format_measure(projects, :suffix => message('widget.size.projects.suffix')) -%> <%= dashboard_configuration.selected_period? ? format_variation(projects) : trend_icon(projects) -%>

<% end %> + + <% if ncloc_language_dist_hash && ncloc_language_dist_hash.size > 1 %> +
+ <% + 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') + %> + + + + + + <% end %> +
+ <% language = languages.find { |l| l.getKey()==language_key.to_s } -%> + <%= language ? language.getName() : language_key -%> +   + <%= barchart(:width => 70, :percent => (100 * language_ncloc.to_i / max).to_i, :tooltip => tooltip)%> +
+ <% end %> 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 index 00000000000..ca77b7c239c --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/language/LanguageDistributionDecorator.java @@ -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 dependsUponMetrics() { + return ImmutableList.of(CoreMetrics.LINES); + } + + @DependedUpon + public List 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); + } + } + +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java b/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java index f4769d38316..1db4a9a5054 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java +++ b/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java @@ -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 index 00000000000..fadb7e82166 --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/language/LanguageDistributionDecoratorTest.java @@ -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 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.emptyList()); + + decorator.decorate(resource, context); + + verify(context, never()).saveMeasure(measureCaptor.capture()); + } + +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index ffc8bc14b22..c8ff38228ea 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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 diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java b/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java index 852678a40af..1ac74b83a42 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java @@ -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") diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/resources/CoreMetricsTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/resources/CoreMetricsTest.java index 1c65ebfb51f..086005be0f8 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/resources/CoreMetricsTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/resources/CoreMetricsTest.java @@ -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 metrics = CoreMetrics.getMetrics(); - assertThat(metrics).hasSize(151); + assertThat(metrics).hasSize(152); assertThat(metrics).contains(CoreMetrics.NCLOC, CoreMetrics.DIRECTORIES); } + } diff --git a/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb b/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb index 08227825e4b..4b1a24aa00d 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb @@ -391,7 +391,7 @@ module ApplicationHelper end align=(percent<0 ? 'float: right;' : nil) - "
" + "
" end def chart(parameters, options={})