diff options
author | Julien HENRY <julien.henry@sonarsource.com> | 2014-01-21 14:55:24 +0100 |
---|---|---|
committer | Julien HENRY <julien.henry@sonarsource.com> | 2014-01-24 16:02:04 +0100 |
commit | 08de7bc30c13fdd63d8e4342a57f4b67d7c15aa9 (patch) | |
tree | 8bb9933ac0606a2b61c53c49dab639ef032ad3a3 | |
parent | 547f5d859251a6c4406fcc94db5faa54362465f3 (diff) | |
download | sonarqube-08de7bc30c13fdd63d8e4342a57f4b67d7c15aa9.tar.gz sonarqube-08de7bc30c13fdd63d8e4342a57f4b67d7c15aa9.zip |
SONAR-926 Multi-language support:
* RulesProfile wrapper
* ModuleFileSystem now support multi language
* Sensors executed trying each language until one is found
31 files changed, 1003 insertions, 370 deletions
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java index 79df113891e..00009b74c6d 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java @@ -20,10 +20,13 @@ package org.sonar.plugins.core; import com.google.common.collect.ImmutableList; -import org.sonar.api.*; +import org.sonar.api.CoreProperties; +import org.sonar.api.Properties; +import org.sonar.api.Property; +import org.sonar.api.PropertyType; +import org.sonar.api.SonarPlugin; import org.sonar.api.checks.NoSonarFilter; import org.sonar.api.config.PropertyDefinition; -import org.sonar.api.resources.Java; import org.sonar.api.resources.Qualifiers; import org.sonar.batch.components.PastSnapshotFinder; import org.sonar.core.technicaldebt.TechnicalDebtConverter; @@ -33,21 +36,91 @@ import org.sonar.plugins.core.charts.DistributionAreaChart; import org.sonar.plugins.core.charts.DistributionBarChart; import org.sonar.plugins.core.charts.XradarChart; import org.sonar.plugins.core.colorizers.JavaColorizerFormat; -import org.sonar.plugins.core.dashboards.*; -import org.sonar.plugins.core.issue.*; +import org.sonar.plugins.core.dashboards.GlobalDefaultDashboard; +import org.sonar.plugins.core.dashboards.ProjectDefaultDashboard; +import org.sonar.plugins.core.dashboards.ProjectHotspotDashboard; +import org.sonar.plugins.core.dashboards.ProjectIssuesDashboard; +import org.sonar.plugins.core.dashboards.ProjectTimeMachineDashboard; +import org.sonar.plugins.core.issue.CountFalsePositivesDecorator; +import org.sonar.plugins.core.issue.CountUnresolvedIssuesDecorator; +import org.sonar.plugins.core.issue.InitialOpenIssuesSensor; +import org.sonar.plugins.core.issue.InitialOpenIssuesStack; +import org.sonar.plugins.core.issue.IssueHandlers; +import org.sonar.plugins.core.issue.IssueTracking; +import org.sonar.plugins.core.issue.IssueTrackingDecorator; +import org.sonar.plugins.core.issue.IssuesDensityDecorator; +import org.sonar.plugins.core.issue.WeightedIssuesDecorator; import org.sonar.plugins.core.issue.ignore.IgnoreIssuesPlugin; -import org.sonar.plugins.core.issue.notification.*; +import org.sonar.plugins.core.issue.notification.ChangesOnMyIssueNotificationDispatcher; +import org.sonar.plugins.core.issue.notification.IssueChangesEmailTemplate; +import org.sonar.plugins.core.issue.notification.NewFalsePositiveNotificationDispatcher; +import org.sonar.plugins.core.issue.notification.NewIssuesEmailTemplate; +import org.sonar.plugins.core.issue.notification.NewIssuesNotificationDispatcher; +import org.sonar.plugins.core.issue.notification.SendIssueNotificationsPostJob; import org.sonar.plugins.core.measurefilters.MyFavouritesFilter; import org.sonar.plugins.core.measurefilters.ProjectFilter; import org.sonar.plugins.core.notifications.alerts.NewAlerts; import org.sonar.plugins.core.security.ApplyProjectRolesDecorator; -import org.sonar.plugins.core.sensors.*; +import org.sonar.plugins.core.sensors.BranchCoverageDecorator; +import org.sonar.plugins.core.sensors.CheckAlertThresholds; +import org.sonar.plugins.core.sensors.CommentDensityDecorator; +import org.sonar.plugins.core.sensors.CoverageDecorator; +import org.sonar.plugins.core.sensors.CoverageMeasurementFilter; +import org.sonar.plugins.core.sensors.DirectoriesDecorator; +import org.sonar.plugins.core.sensors.FileHashSensor; +import org.sonar.plugins.core.sensors.FilesDecorator; +import org.sonar.plugins.core.sensors.GenerateAlertEvents; +import org.sonar.plugins.core.sensors.ItBranchCoverageDecorator; +import org.sonar.plugins.core.sensors.ItCoverageDecorator; +import org.sonar.plugins.core.sensors.ItLineCoverageDecorator; +import org.sonar.plugins.core.sensors.LineCoverageDecorator; +import org.sonar.plugins.core.sensors.ManualMeasureDecorator; +import org.sonar.plugins.core.sensors.OverallBranchCoverageDecorator; +import org.sonar.plugins.core.sensors.OverallCoverageDecorator; +import org.sonar.plugins.core.sensors.OverallLineCoverageDecorator; +import org.sonar.plugins.core.sensors.ProfileEventsSensor; +import org.sonar.plugins.core.sensors.ProfileSensor; +import org.sonar.plugins.core.sensors.ProjectLinksSensor; +import org.sonar.plugins.core.sensors.UnitTestDecorator; +import org.sonar.plugins.core.sensors.VersionEventsSensor; import org.sonar.plugins.core.technicaldebt.NewTechnicalDebtDecorator; import org.sonar.plugins.core.technicaldebt.TechnicalDebtDecorator; -import org.sonar.plugins.core.timemachine.*; +import org.sonar.plugins.core.timemachine.NewCoverageAggregator; +import org.sonar.plugins.core.timemachine.NewCoverageFileAnalyzer; +import org.sonar.plugins.core.timemachine.NewItCoverageFileAnalyzer; +import org.sonar.plugins.core.timemachine.NewOverallCoverageFileAnalyzer; +import org.sonar.plugins.core.timemachine.TendencyDecorator; +import org.sonar.plugins.core.timemachine.TimeMachineConfigurationPersister; +import org.sonar.plugins.core.timemachine.VariationDecorator; import org.sonar.plugins.core.web.TestsViewer; -import org.sonar.plugins.core.widgets.*; -import org.sonar.plugins.core.widgets.issues.*; +import org.sonar.plugins.core.widgets.AlertsWidget; +import org.sonar.plugins.core.widgets.BubbleChartWidget; +import org.sonar.plugins.core.widgets.ComplexityWidget; +import org.sonar.plugins.core.widgets.CoverageWidget; +import org.sonar.plugins.core.widgets.CustomMeasuresWidget; +import org.sonar.plugins.core.widgets.DescriptionWidget; +import org.sonar.plugins.core.widgets.DocumentationCommentsWidget; +import org.sonar.plugins.core.widgets.DuplicationsWidget; +import org.sonar.plugins.core.widgets.EventsWidget; +import org.sonar.plugins.core.widgets.HotspotMetricWidget; +import org.sonar.plugins.core.widgets.HotspotMostViolatedResourcesWidget; +import org.sonar.plugins.core.widgets.HotspotMostViolatedRulesWidget; +import org.sonar.plugins.core.widgets.ItCoverageWidget; +import org.sonar.plugins.core.widgets.MeasureFilterListWidget; +import org.sonar.plugins.core.widgets.MeasureFilterTreemapWidget; +import org.sonar.plugins.core.widgets.SizeWidget; +import org.sonar.plugins.core.widgets.TechnicalDebtPyramidWidget; +import org.sonar.plugins.core.widgets.TimeMachineWidget; +import org.sonar.plugins.core.widgets.TimelineWidget; +import org.sonar.plugins.core.widgets.TreemapWidget; +import org.sonar.plugins.core.widgets.WelcomeWidget; +import org.sonar.plugins.core.widgets.issues.ActionPlansWidget; +import org.sonar.plugins.core.widgets.issues.FalsePositiveIssuesWidget; +import org.sonar.plugins.core.widgets.issues.IssueFilterWidget; +import org.sonar.plugins.core.widgets.issues.IssuesWidget; +import org.sonar.plugins.core.widgets.issues.MyUnresolvedIssuesWidget; +import org.sonar.plugins.core.widgets.issues.UnresolvedIssuesPerAssigneeWidget; +import org.sonar.plugins.core.widgets.issues.UnresolvedIssuesStatusesWidget; import org.sonar.plugins.core.widgets.measures.MeasureFilterAsBubbleChartWidget; import org.sonar.plugins.core.widgets.measures.MeasureFilterAsHistogramWidget; import org.sonar.plugins.core.widgets.measures.MeasureFilterAsPieChartWidget; @@ -113,9 +186,8 @@ import java.util.List; category = CoreProperties.CATEGORY_GENERAL), @Property( key = CoreProperties.PROJECT_LANGUAGE_PROPERTY, - defaultValue = Java.KEY, name = "Default language", - description = "Default language of the source code to analyse", + description = "[Deprecated] Default language of the source code to analyse. Keep it blank to allow multi-language analysis.", project = false, global = true, category = CoreProperties.CATEGORY_GENERAL), diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileEventsSensor.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileEventsSensor.java index 14db37632f3..39e722c099e 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileEventsSensor.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileEventsSensor.java @@ -29,6 +29,8 @@ import org.sonar.api.measures.Measure; import org.sonar.api.measures.Metric; import org.sonar.api.profiles.RulesProfile; import org.sonar.api.resources.Project; +import org.sonar.batch.RulesProfileWrapper; +import org.sonar.batch.scan.language.ModuleLanguages; import java.util.List; @@ -36,41 +38,47 @@ public class ProfileEventsSensor implements Sensor { private final RulesProfile profile; private final TimeMachine timeMachine; + private final ModuleLanguages moduleLanguages; - public ProfileEventsSensor(RulesProfile profile, TimeMachine timeMachine) { + public ProfileEventsSensor(RulesProfile profile, TimeMachine timeMachine, ModuleLanguages moduleLanguages) { this.profile = profile; this.timeMachine = timeMachine; + this.moduleLanguages = moduleLanguages; } public boolean shouldExecuteOnProject(Project project) { - // Views will define a fake profile with a null id - return profile.getId() != null; + // Views will define a fake profile + return profile instanceof RulesProfileWrapper; } public void analyse(Project project, SensorContext context) { - Measure pastProfileMeasure = getPreviousMeasure(project, CoreMetrics.PROFILE); - if (pastProfileMeasure == null) { - // first analysis - return; - } - int pastProfileId = pastProfileMeasure.getIntValue(); - Measure pastProfileVersionMeasure = getPreviousMeasure(project, CoreMetrics.PROFILE_VERSION); - final int pastProfileVersion; - // first analysis with versions - if (pastProfileVersionMeasure == null) { - pastProfileVersion = 1; - } else { - pastProfileVersion = pastProfileVersionMeasure.getIntValue(); - } - String pastProfile = formatProfileDescription(pastProfileMeasure.getData(), pastProfileVersion); + RulesProfileWrapper profileWrapper = (RulesProfileWrapper) profile; + for (String languageKey : moduleLanguages.getModuleLanguageKeys()) { + RulesProfile realProfile = profileWrapper.getProfileByLanguage(languageKey); + Measure pastProfileMeasure = getPreviousMeasure(project, CoreMetrics.PROFILE); + if (pastProfileMeasure == null) { + // first analysis + return; + } + int pastProfileId = pastProfileMeasure.getIntValue(); + Measure pastProfileVersionMeasure = getPreviousMeasure(project, CoreMetrics.PROFILE_VERSION); + final int pastProfileVersion; + // first analysis with versions + if (pastProfileVersionMeasure == null) { + pastProfileVersion = 1; + } else { + pastProfileVersion = pastProfileVersionMeasure.getIntValue(); + } + String pastProfile = formatProfileDescription(pastProfileMeasure.getData(), pastProfileVersion); - int currentProfileId = profile.getId(); - int currentProfileVersion = profile.getVersion(); - String currentProfile = formatProfileDescription(profile.getName(), currentProfileVersion); + int currentProfileId = realProfile.getId(); + int currentProfileVersion = realProfile.getVersion(); + String currentProfile = formatProfileDescription(realProfile.getName(), currentProfileVersion); - if ((pastProfileId != currentProfileId) || (pastProfileVersion != currentProfileVersion)) { - // A different profile is used for this project or new version of same profile - context.createEvent(project, currentProfile, currentProfile + " is used instead of " + pastProfile, Event.CATEGORY_PROFILE, null); + if ((pastProfileId != currentProfileId) || (pastProfileVersion != currentProfileVersion)) { + // A different profile is used for this project or new version of same profile + context.createEvent(project, currentProfile, currentProfile + " is used instead of " + pastProfile, Event.CATEGORY_PROFILE, null); + } } } @@ -80,8 +88,8 @@ public class ProfileEventsSensor implements Sensor { private Measure getPreviousMeasure(Project project, Metric metric) { TimeMachineQuery query = new TimeMachineQuery(project) - .setOnlyLastAnalysis(true) - .setMetrics(metric); + .setOnlyLastAnalysis(true) + .setMetrics(metric); List<Measure> measures = timeMachine.getMeasures(query); if (measures.isEmpty()) { return null; diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileSensor.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileSensor.java index d59b9ded823..ffb6252101c 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileSensor.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileSensor.java @@ -26,33 +26,41 @@ import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.Measure; import org.sonar.api.profiles.RulesProfile; import org.sonar.api.resources.Project; +import org.sonar.batch.RulesProfileWrapper; +import org.sonar.batch.scan.language.ModuleLanguages; public class ProfileSensor implements Sensor { private final RulesProfile profile; private final DatabaseSession session; + private ModuleLanguages languages; - public ProfileSensor(RulesProfile profile, DatabaseSession session) { + public ProfileSensor(RulesProfile profile, DatabaseSession session, ModuleLanguages languages) { this.profile = profile; this.session = session; + this.languages = languages; } public boolean shouldExecuteOnProject(Project project) { - // Views will define a fake profile with a null id - return profile.getId() != null; + // Views will define a fake profile + return profile instanceof RulesProfileWrapper; } public void analyse(Project project, SensorContext context) { - Measure measure = new Measure(CoreMetrics.PROFILE, profile.getName()); - Measure measureVersion = new Measure(CoreMetrics.PROFILE_VERSION, Integer.valueOf(profile.getVersion()).doubleValue()); - if (profile.getId() != null) { - measure.setValue(profile.getId().doubleValue()); + RulesProfileWrapper wrapper = (RulesProfileWrapper) profile; + for (String languageKey : languages.getModuleLanguageKeys()) { + RulesProfile realProfile = wrapper.getProfileByLanguage(languageKey); + Measure measure = new Measure(CoreMetrics.PROFILE, profile.getName()); + Measure measureVersion = new Measure(CoreMetrics.PROFILE_VERSION, Integer.valueOf(profile.getVersion()).doubleValue()); + if (realProfile.getId() != null) { + measure.setValue(realProfile.getId().doubleValue()); - profile.setUsed(true); - session.merge(profile); + realProfile.setUsed(true); + session.merge(realProfile); + } + context.saveMeasure(measure); + context.saveMeasure(measureVersion); } - context.saveMeasure(measure); - context.saveMeasure(measureVersion); } @Override diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileEventsSensorTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileEventsSensorTest.java index f7a44372623..84d586a5874 100644 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileEventsSensorTest.java +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileEventsSensorTest.java @@ -25,47 +25,66 @@ import org.sonar.api.batch.Event; import org.sonar.api.batch.SensorContext; import org.sonar.api.batch.TimeMachine; import org.sonar.api.batch.TimeMachineQuery; +import org.sonar.api.config.Settings; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.Measure; import org.sonar.api.profiles.RulesProfile; +import org.sonar.api.resources.Java; +import org.sonar.api.resources.Languages; import org.sonar.api.resources.Project; +import org.sonar.batch.RulesProfileWrapper; +import org.sonar.batch.scan.language.ModuleLanguages; import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.HashMap; +import java.util.Map; import static org.fest.assertions.Assertions.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.same; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; public class ProfileEventsSensorTest { private Project project; private SensorContext context; + private ModuleLanguages moduleLanguages; + private RulesProfileWrapper wrapper; + private RulesProfile profile; @Before public void prepare() { project = mock(Project.class); context = mock(SensorContext.class); + + moduleLanguages = new ModuleLanguages(new Settings(), new Languages(Java.INSTANCE)); + moduleLanguages.addLanguage("java"); + Map<String, RulesProfile> ruleProfilesPerLanguages = new HashMap<String, RulesProfile>(); + profile = mock(RulesProfile.class); + ruleProfilesPerLanguages.put("java", profile); + wrapper = new RulesProfileWrapper(moduleLanguages, ruleProfilesPerLanguages); } @Test public void shouldExecuteWhenProfileWithId() { - RulesProfile profile = mock(RulesProfile.class); when(profile.getId()).thenReturn(123); - ProfileEventsSensor sensor = new ProfileEventsSensor(profile, null); + ProfileEventsSensor sensor = new ProfileEventsSensor(wrapper, null, moduleLanguages); assertThat(sensor.shouldExecuteOnProject(project)).isTrue(); verifyZeroInteractions(project); } @Test - public void shouldNotExecuteIfProfileWithoutId() { + public void shouldNotExecuteIfProfileIsNotWrapper() { RulesProfile profile = mock(RulesProfile.class); when(profile.getId()).thenReturn(null); - ProfileEventsSensor sensor = new ProfileEventsSensor(profile, null); + ProfileEventsSensor sensor = new ProfileEventsSensor(profile, null, moduleLanguages); assertThat(sensor.shouldExecuteOnProject(project)).isFalse(); verifyZeroInteractions(project); @@ -73,9 +92,9 @@ public class ProfileEventsSensorTest { @Test public void shouldDoNothingIfNoProfileChange() { - RulesProfile profile = mockProfileWithVersion(1); + mockProfileWithVersion(1); TimeMachine timeMachine = mockTM(22.0, "Foo", 1.0); // Same profile, same version - ProfileEventsSensor sensor = new ProfileEventsSensor(profile, timeMachine); + ProfileEventsSensor sensor = new ProfileEventsSensor(wrapper, timeMachine, moduleLanguages); sensor.analyse(project, context); @@ -84,9 +103,9 @@ public class ProfileEventsSensorTest { @Test public void shouldCreateEventIfProfileChange() { - RulesProfile profile = mockProfileWithVersion(1); + mockProfileWithVersion(1); TimeMachine timeMachine = mockTM(21.0, "Bar", 1.0); // Different profile - ProfileEventsSensor sensor = new ProfileEventsSensor(profile, timeMachine); + ProfileEventsSensor sensor = new ProfileEventsSensor(wrapper, timeMachine, moduleLanguages); sensor.analyse(project, context); @@ -98,9 +117,9 @@ public class ProfileEventsSensorTest { @Test public void shouldCreateEventIfProfileVersionChange() { - RulesProfile profile = mockProfileWithVersion(2); + mockProfileWithVersion(2); TimeMachine timeMachine = mockTM(22.0, "Foo", 1.0); // Same profile, different version - ProfileEventsSensor sensor = new ProfileEventsSensor(profile, timeMachine); + ProfileEventsSensor sensor = new ProfileEventsSensor(wrapper, timeMachine, moduleLanguages); sensor.analyse(project, context); @@ -112,9 +131,9 @@ public class ProfileEventsSensorTest { @Test public void shouldNotCreateEventIfFirstAnalysis() { - RulesProfile profile = mockProfileWithVersion(2); + mockProfileWithVersion(2); TimeMachine timeMachine = mockTM(null, null); - ProfileEventsSensor sensor = new ProfileEventsSensor(profile, timeMachine); + ProfileEventsSensor sensor = new ProfileEventsSensor(wrapper, timeMachine, moduleLanguages); sensor.analyse(project, context); @@ -123,9 +142,9 @@ public class ProfileEventsSensorTest { @Test public void shouldCreateEventIfFirstAnalysisWithVersionsAndVersionMoreThan1() { - RulesProfile profile = mockProfileWithVersion(2); + mockProfileWithVersion(2); TimeMachine timeMachine = mockTM(22.0, "Foo", null); - ProfileEventsSensor sensor = new ProfileEventsSensor(profile, timeMachine); + ProfileEventsSensor sensor = new ProfileEventsSensor(wrapper, timeMachine, moduleLanguages); sensor.analyse(project, context); @@ -135,12 +154,10 @@ public class ProfileEventsSensorTest { same(Event.CATEGORY_PROFILE), any(Date.class)); } - private RulesProfile mockProfileWithVersion(int version) { - RulesProfile profile = mock(RulesProfile.class); + private void mockProfileWithVersion(int version) { when(profile.getId()).thenReturn(22); when(profile.getName()).thenReturn("Foo"); when(profile.getVersion()).thenReturn(version); - return profile; } private TimeMachine mockTM(double profileId, String profileName, Double versionValue) { diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileSensorTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileSensorTest.java index cce7bab1087..4aa3ae589e5 100644 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileSensorTest.java +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileSensorTest.java @@ -21,13 +21,23 @@ package org.sonar.plugins.core.sensors; import org.junit.Test; import org.sonar.api.batch.SensorContext; +import org.sonar.api.config.Settings; import org.sonar.api.database.DatabaseSession; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.profiles.RulesProfile; +import org.sonar.api.resources.Java; +import org.sonar.api.resources.Languages; import org.sonar.api.test.IsMeasure; +import org.sonar.batch.RulesProfileWrapper; +import org.sonar.batch.scan.language.ModuleLanguages; + +import java.util.HashMap; +import java.util.Map; import static org.mockito.Matchers.argThat; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class ProfileSensorTest { @@ -37,10 +47,17 @@ public class ProfileSensorTest { when(profile.getId()).thenReturn(22); when(profile.getName()).thenReturn("fake"); when(profile.getVersion()).thenReturn(2); + + ModuleLanguages moduleLanguages = new ModuleLanguages(new Settings(), new Languages(Java.INSTANCE)); + moduleLanguages.addLanguage("java"); + Map<String, RulesProfile> ruleProfilesPerLanguages = new HashMap<String, RulesProfile>(); + ruleProfilesPerLanguages.put("java", profile); + RulesProfileWrapper wrapper = new RulesProfileWrapper(moduleLanguages, ruleProfilesPerLanguages); + SensorContext context = mock(SensorContext.class); DatabaseSession session = mock(DatabaseSession.class); - ProfileSensor sensor = new ProfileSensor(profile, session); + ProfileSensor sensor = new ProfileSensor(wrapper, session, moduleLanguages); sensor.analyse(null, context); verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.PROFILE, 22d))); diff --git a/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/CpdSensor.java b/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/CpdSensor.java index d83bb5428d4..47c3c15e55d 100644 --- a/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/CpdSensor.java +++ b/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/CpdSensor.java @@ -26,7 +26,9 @@ import org.sonar.api.CoreProperties; import org.sonar.api.batch.Sensor; import org.sonar.api.batch.SensorContext; import org.sonar.api.config.Settings; +import org.sonar.api.resources.Language; import org.sonar.api.resources.Project; +import org.sonar.batch.scan.language.ModuleLanguages; public class CpdSensor implements Sensor { @@ -35,30 +37,22 @@ public class CpdSensor implements Sensor { private CpdEngine sonarEngine; private CpdEngine sonarBridgeEngine; private Settings settings; + private ModuleLanguages moduleLanguages; - public CpdSensor(SonarEngine sonarEngine, SonarBridgeEngine sonarBridgeEngine, Settings settings) { + public CpdSensor(SonarEngine sonarEngine, SonarBridgeEngine sonarBridgeEngine, Settings settings, ModuleLanguages moduleLanguages) { this.sonarEngine = sonarEngine; this.sonarBridgeEngine = sonarBridgeEngine; this.settings = settings; + this.moduleLanguages = moduleLanguages; } public boolean shouldExecuteOnProject(Project project) { - if (isSkipped(project)) { - LOG.info("Detection of duplicated code is skipped"); - return false; - } - - if (!getEngine(project).isLanguageSupported(project.getLanguage())) { - LOG.debug("Detection of duplicated code is not supported for {}.", project.getLanguage()); - return false; - } - return true; } @VisibleForTesting - CpdEngine getEngine(Project project) { - if (sonarEngine.isLanguageSupported(project.getLanguage())) { + CpdEngine getEngine(Language language) { + if (sonarEngine.isLanguageSupported(language)) { return sonarEngine; } else { return sonarBridgeEngine; @@ -66,8 +60,8 @@ public class CpdSensor implements Sensor { } @VisibleForTesting - boolean isSkipped(Project project) { - String key = "sonar.cpd." + project.getLanguageKey() + ".skip"; + boolean isSkipped(Language language) { + String key = "sonar.cpd." + language.getKey() + ".skip"; if (settings.hasKey(key)) { return settings.getBoolean(key); } @@ -75,9 +69,20 @@ public class CpdSensor implements Sensor { } public void analyse(Project project, SensorContext context) { - CpdEngine engine = getEngine(project); - LOG.info("{} is used", engine); - engine.analyse(project, context); + for (Language language : moduleLanguages.getModuleLanguages()) { + if (isSkipped(language)) { + LOG.info("Detection of duplicated code is skipped for {}.", language); + continue; + } + + CpdEngine engine = getEngine(language); + if (!engine.isLanguageSupported(language)) { + LOG.debug("Detection of duplicated code is not supported for {}.", language); + continue; + } + LOG.info("{} is used for {}", engine, language); + engine.analyse(project, context); + } } @Override diff --git a/plugins/sonar-cpd-plugin/src/test/java/org/sonar/plugins/cpd/CpdSensorTest.java b/plugins/sonar-cpd-plugin/src/test/java/org/sonar/plugins/cpd/CpdSensorTest.java index 0cf3ade848b..d410d658eeb 100644 --- a/plugins/sonar-cpd-plugin/src/test/java/org/sonar/plugins/cpd/CpdSensorTest.java +++ b/plugins/sonar-cpd-plugin/src/test/java/org/sonar/plugins/cpd/CpdSensorTest.java @@ -19,14 +19,15 @@ */ package org.sonar.plugins.cpd; -import org.apache.commons.configuration.PropertiesConfiguration; import org.junit.Before; import org.junit.Test; import org.sonar.api.config.PropertyDefinitions; import org.sonar.api.config.Settings; +import org.sonar.api.resources.AbstractLanguage; import org.sonar.api.resources.Java; import org.sonar.api.resources.Language; -import org.sonar.api.resources.Project; +import org.sonar.api.resources.Languages; +import org.sonar.batch.scan.language.ModuleLanguages; import org.sonar.plugins.cpd.index.IndexFactory; import static org.fest.assertions.Assertions.assertThat; @@ -38,6 +39,7 @@ public class CpdSensorTest { SonarBridgeEngine sonarBridgeEngine; CpdSensor sensor; Settings settings; + private Language phpLanguage; @Before public void setUp() { @@ -45,45 +47,40 @@ public class CpdSensorTest { sonarEngine = new SonarEngine(indexFactory, null, null); sonarBridgeEngine = new SonarBridgeEngine(indexFactory, null, null); settings = new Settings(new PropertyDefinitions(CpdPlugin.class)); - sensor = new CpdSensor(sonarEngine, sonarBridgeEngine, settings); + phpLanguage = new AbstractLanguage("php", "PHP") { + + @Override + public String[] getFileSuffixes() { + return null; + } + }; + sensor = new CpdSensor(sonarEngine, sonarBridgeEngine, settings, new ModuleLanguages(settings, new Languages())); } @Test public void test_global_skip() { settings.setProperty("sonar.cpd.skip", true); - assertThat(sensor.isSkipped(createJavaProject())).isTrue(); + assertThat(sensor.isSkipped(Java.INSTANCE)).isTrue(); } @Test public void should_not_skip_by_default() { - assertThat(sensor.isSkipped(createJavaProject())).isFalse(); + assertThat(sensor.isSkipped(Java.INSTANCE)).isFalse(); } @Test public void should_skip_by_language() { settings.setProperty("sonar.cpd.skip", false); settings.setProperty("sonar.cpd.php.skip", true); - assertThat(sensor.isSkipped(createPhpProject())).isTrue(); - assertThat(sensor.isSkipped(createJavaProject())).isFalse(); + + assertThat(sensor.isSkipped(phpLanguage)).isTrue(); + assertThat(sensor.isSkipped(Java.INSTANCE)).isFalse(); } @Test public void test_engine() { - assertThat(sensor.getEngine(createJavaProject())).isSameAs(sonarEngine); - assertThat(sensor.getEngine(createPhpProject())).isSameAs(sonarBridgeEngine); - } - - private Project createJavaProject() { - PropertiesConfiguration conf = new PropertiesConfiguration(); - conf.setProperty("sonar.language", "java"); - return new Project("java_project").setConfiguration(conf).setLanguage(Java.INSTANCE); - } - - private Project createPhpProject() { - PropertiesConfiguration conf = new PropertiesConfiguration(); - conf.setProperty("sonar.language", "php"); - Language phpLanguage = mock(Language.class); - return new Project("php_project").setConfiguration(conf).setLanguage(phpLanguage); + assertThat(sensor.getEngine(Java.INSTANCE)).isSameAs(sonarEngine); + assertThat(sensor.getEngine(phpLanguage)).isSameAs(sonarBridgeEngine); } } diff --git a/sonar-batch/src/main/java/org/sonar/batch/DefaultProfileLoader.java b/sonar-batch/src/main/java/org/sonar/batch/DefaultProfileLoader.java index 8e8e02aa5c1..797723590d0 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/DefaultProfileLoader.java +++ b/sonar-batch/src/main/java/org/sonar/batch/DefaultProfileLoader.java @@ -24,45 +24,56 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.config.Settings; import org.sonar.api.profiles.RulesProfile; +import org.sonar.api.resources.Language; +import org.sonar.api.resources.Languages; import org.sonar.api.resources.Project; import org.sonar.api.rules.ActiveRule; import org.sonar.api.utils.SonarException; +import org.sonar.batch.scan.language.ModuleLanguages; import org.sonar.jpa.dao.ProfilesDao; +import java.util.HashMap; +import java.util.Map; + public class DefaultProfileLoader implements ProfileLoader { private static final Logger LOG = LoggerFactory.getLogger(DefaultProfileLoader.class); private ProfilesDao dao; + private Languages languages; + private ModuleLanguages moduleLanguages; - public DefaultProfileLoader(ProfilesDao dao) { + public DefaultProfileLoader(ProfilesDao dao, ModuleLanguages moduleLanguages, Languages languages) { this.dao = dao; + this.moduleLanguages = moduleLanguages; + this.languages = languages; } - public RulesProfile load(Project project, Settings settings) { + public RulesProfile load(Project module, Settings settings) { + Map<String, RulesProfile> profilesPerLanguageKey = new HashMap<String, RulesProfile>(); + + // TODO For now we have to load profile of all languages even if module will only use part of them because ModuleLanguages may not be + // initialized yet + for (Language language : languages.all()) { + String languageKey = language.getKey(); - String profileName = StringUtils.defaultIfBlank( + String profileName = StringUtils.defaultIfBlank( settings.getString("sonar.profile"), - settings.getString("sonar.profile." + project.getLanguageKey())); + settings.getString("sonar.profile." + languageKey)); - if (StringUtils.isBlank(profileName)) { - // This means that the current language is not supported by any installed plugin, otherwise at least a - // "Default <Language Name>" profile would have been created by ActivateDefaultProfiles class. - String msg = "You must install a plugin that supports the language key '" + project.getLanguageKey() + "'."; - if (!LOG.isDebugEnabled()) { - msg += " Run analysis in verbose mode to see list of available language keys."; - } else { - msg += " See analysis log for a list of available language keys."; + RulesProfile profile = dao.getProfile(languageKey, profileName); + if (profile == null) { + throw new SonarException("Quality profile not found : " + profileName + ", language " + languageKey); } - throw new SonarException(msg); - } - RulesProfile profile = dao.getProfile(project.getLanguageKey(), profileName); - if (profile == null) { - throw new SonarException("Quality profile not found : " + profileName + ", language " + project.getLanguageKey()); + profilesPerLanguageKey.put(languageKey, hibernateHack(profile)); } - return hibernateHack(profile); + RulesProfile profile = new RulesProfileWrapper(moduleLanguages, profilesPerLanguageKey); + for (Map.Entry<String, RulesProfile> profiles : profilesPerLanguageKey.entrySet()) { + LOG.info("Quality profile for {}: {}", profiles.getKey(), profiles.getValue()); + } + return profile; } private RulesProfile hibernateHack(RulesProfile profile) { diff --git a/sonar-batch/src/main/java/org/sonar/batch/ProfileLoader.java b/sonar-batch/src/main/java/org/sonar/batch/ProfileLoader.java index c844cef918f..51ce91c7b37 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/ProfileLoader.java +++ b/sonar-batch/src/main/java/org/sonar/batch/ProfileLoader.java @@ -23,6 +23,10 @@ import org.sonar.api.config.Settings; import org.sonar.api.profiles.RulesProfile; import org.sonar.api.resources.Project; +/** + * This interface is implemented by the views plugin!! + * + */ public interface ProfileLoader { /** diff --git a/sonar-batch/src/main/java/org/sonar/batch/ProfileProvider.java b/sonar-batch/src/main/java/org/sonar/batch/ProfileProvider.java index 93012a107d2..e73f13a341b 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/ProfileProvider.java +++ b/sonar-batch/src/main/java/org/sonar/batch/ProfileProvider.java @@ -20,8 +20,6 @@ package org.sonar.batch; import org.picocontainer.injectors.ProviderAdapter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.sonar.api.config.Settings; import org.sonar.api.profiles.RulesProfile; import org.sonar.api.resources.Languages; @@ -29,8 +27,6 @@ import org.sonar.api.resources.Project; public class ProfileProvider extends ProviderAdapter { - private static final Logger LOG = LoggerFactory.getLogger(ProfileProvider.class); - private RulesProfile profile; /** @@ -39,7 +35,6 @@ public class ProfileProvider extends ProviderAdapter { public RulesProfile provide(Project project, ProfileLoader profileLoader, Settings settings, Languages languages) { if (profile == null) { profile = profileLoader.load(project, settings); - LOG.info("Quality profile : {}", profile); } return profile; } diff --git a/sonar-batch/src/main/java/org/sonar/batch/RulesProfileWrapper.java b/sonar-batch/src/main/java/org/sonar/batch/RulesProfileWrapper.java new file mode 100644 index 00000000000..8fcd6edf8b3 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/RulesProfileWrapper.java @@ -0,0 +1,150 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 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; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.profiles.Alert; +import org.sonar.api.profiles.RulesProfile; +import org.sonar.api.rules.ActiveRule; +import org.sonar.api.rules.Rule; +import org.sonar.api.utils.SonarException; +import org.sonar.batch.scan.language.ModuleLanguages; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * This wrapper is used to try to preserver backward compatibility for plugins that used to depends on {@link RulesProfile} + * @since 4.2 + */ +public class RulesProfileWrapper extends RulesProfile { + + private static final Logger LOG = LoggerFactory.getLogger(RulesProfileWrapper.class); + + private final Map<String, RulesProfile> ruleProfilesPerLanguages; + private ModuleLanguages moduleLanguages; + + public RulesProfileWrapper(ModuleLanguages moduleLanguages, Map<String, RulesProfile> ruleProfilesPerLanguages) { + this.moduleLanguages = moduleLanguages; + this.ruleProfilesPerLanguages = ruleProfilesPerLanguages; + } + + @Override + public Integer getId() { + return getSingleProfileOrFail().getId(); + } + + private RulesProfile getSingleProfileOrFail() { + if (moduleLanguages.getModuleLanguageKeys().size() != 1) { + throw new SonarException("Please update your plugin to support multi-language analysis"); + } + return ruleProfilesPerLanguages.get(moduleLanguages.getModuleLanguageKeys().iterator().next()); + } + + public RulesProfile getProfileByLanguage(String languageKey) { + return ruleProfilesPerLanguages.get(languageKey); + } + + @Override + public String getName() { + return getSingleProfileOrFail().getName(); + } + + @Override + public String getLanguage() { + if (moduleLanguages.getModuleLanguageKeys().size() != 1) { + // FIXME This is a hack for CommonChecksDecorator that call this method in its constructor + LOG.debug("Please update your plugin to support multi-language analysis", new SonarException("Please update your plugin to support multi-language analysis")); + return ""; + } + return ruleProfilesPerLanguages.get(moduleLanguages.getModuleLanguageKeys().iterator().next()).getLanguage(); + } + + @Override + public List<Alert> getAlerts() { + List<Alert> result = new ArrayList<Alert>(); + for (RulesProfile profile : ruleProfilesPerLanguages.values()) { + result.addAll(profile.getAlerts()); + } + return result; + } + + @Override + public List<ActiveRule> getActiveRules() { + List<ActiveRule> activeRules = new ArrayList<ActiveRule>(); + for (RulesProfile profile : ruleProfilesPerLanguages.values()) { + activeRules.addAll(profile.getActiveRules()); + } + return activeRules; + } + + @Override + public int getVersion() { + return getSingleProfileOrFail().getVersion(); + } + + @Override + public ActiveRule getActiveRule(String repositoryKey, String ruleKey) { + for (RulesProfile profile : ruleProfilesPerLanguages.values()) { + ActiveRule activeRule = profile.getActiveRule(repositoryKey, ruleKey); + if (activeRule != null) { + return activeRule; + } + } + return null; + } + + @Override + public List<ActiveRule> getActiveRulesByRepository(String repositoryKey) { + List<ActiveRule> activeRules = new ArrayList<ActiveRule>(); + for (RulesProfile profile : ruleProfilesPerLanguages.values()) { + activeRules.addAll(profile.getActiveRulesByRepository(repositoryKey)); + } + return activeRules; + } + + @Override + public List<ActiveRule> getActiveRules(boolean acceptDisabledRules) { + List<ActiveRule> activeRules = new ArrayList<ActiveRule>(); + for (RulesProfile profile : ruleProfilesPerLanguages.values()) { + activeRules.addAll(profile.getActiveRules(acceptDisabledRules)); + } + return activeRules; + } + + @Override + public ActiveRule getActiveRule(Rule rule) { + for (RulesProfile profile : ruleProfilesPerLanguages.values()) { + ActiveRule activeRule = profile.getActiveRule(rule); + if (activeRule != null) { + return activeRule; + } + } + return null; + } + + @Override + public Boolean getDefaultProfile() { + return getSingleProfileOrFail().getDefaultProfile(); + } + +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchExtensionDictionnary.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchExtensionDictionnary.java index 6e246f19f0f..3779471971c 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchExtensionDictionnary.java +++ b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchExtensionDictionnary.java @@ -23,6 +23,7 @@ import com.google.common.collect.Lists; import org.apache.commons.lang.ClassUtils; import org.sonar.api.BatchExtension; import org.sonar.api.batch.CheckProject; +import org.sonar.api.batch.Sensor; import org.sonar.api.platform.ComponentContainer; import org.sonar.api.resources.Project; @@ -58,7 +59,9 @@ public class BatchExtensionDictionnary extends org.sonar.api.batch.BatchExtensio private boolean shouldKeep(Class type, Object extension, Project project, ExtensionMatcher matcher) { boolean keep = ClassUtils.isAssignable(extension.getClass(), type) && (matcher == null || matcher.accept(extension)); - if (keep && project != null && ClassUtils.isAssignable(extension.getClass(), CheckProject.class)) { + // For Sensors we no longer filter on shouldExecuteOnProject + if (keep && project != null && ClassUtils.isAssignable(extension.getClass(), CheckProject.class) + && !ClassUtils.isAssignable(extension.getClass(), Sensor.class)) { keep = ((CheckProject) extension).shouldExecuteOnProject(project); } return keep; diff --git a/sonar-batch/src/main/java/org/sonar/batch/phases/InitializersExecutor.java b/sonar-batch/src/main/java/org/sonar/batch/phases/InitializersExecutor.java index 2c26c3f002b..f0cb2033377 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/phases/InitializersExecutor.java +++ b/sonar-batch/src/main/java/org/sonar/batch/phases/InitializersExecutor.java @@ -71,9 +71,7 @@ public class InitializersExecutor { eventBus.fireEvent(new InitializerExecutionEvent(initializer, false)); } - if (!initializers.isEmpty()) { - fs.index(); - } + fs.index(); eventBus.fireEvent(new InitializersPhaseEvent(Lists.newArrayList(initializers), false)); } diff --git a/sonar-batch/src/main/java/org/sonar/batch/phases/PhaseExecutor.java b/sonar-batch/src/main/java/org/sonar/batch/phases/PhaseExecutor.java index e987b4f17df..e4f6f8d92aa 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/phases/PhaseExecutor.java +++ b/sonar-batch/src/main/java/org/sonar/batch/phases/PhaseExecutor.java @@ -29,10 +29,10 @@ import org.sonar.batch.events.EventBus; import org.sonar.batch.index.DefaultIndex; import org.sonar.batch.index.PersistenceManager; import org.sonar.batch.index.ScanPersister; -import org.sonar.batch.scan.report.JsonReport; import org.sonar.batch.scan.filesystem.FileSystemLogger; import org.sonar.batch.scan.maven.MavenPhaseExecutor; import org.sonar.batch.scan.maven.MavenPluginsConfigurator; +import org.sonar.batch.scan.report.JsonReport; import java.util.Collection; @@ -41,9 +41,9 @@ public final class PhaseExecutor { public static final Logger LOGGER = LoggerFactory.getLogger(PhaseExecutor.class); public static Collection<Class> getPhaseClasses() { - return Lists.<Class> newArrayList(DecoratorsExecutor.class, MavenPhaseExecutor.class, MavenPluginsConfigurator.class, - PostJobsExecutor.class, SensorsExecutor.class, - InitializersExecutor.class, ProjectInitializer.class, UpdateStatusJob.class); + return Lists.<Class>newArrayList(DecoratorsExecutor.class, MavenPhaseExecutor.class, MavenPluginsConfigurator.class, + PostJobsExecutor.class, SensorsExecutor.class, + InitializersExecutor.class, ProjectInitializer.class, UpdateStatusJob.class); } private EventBus eventBus; @@ -64,11 +64,11 @@ public final class PhaseExecutor { private final JsonReport jsonReport; public PhaseExecutor(Phases phases, DecoratorsExecutor decoratorsExecutor, MavenPhaseExecutor mavenPhaseExecutor, - MavenPluginsConfigurator mavenPluginsConfigurator, InitializersExecutor initializersExecutor, - PostJobsExecutor postJobsExecutor, SensorsExecutor sensorsExecutor, - PersistenceManager persistenceManager, SensorContext sensorContext, DefaultIndex index, - EventBus eventBus, UpdateStatusJob updateStatusJob, ProjectInitializer pi, - ScanPersister[] persisters, FileSystemLogger fsLogger, JsonReport jsonReport) { + MavenPluginsConfigurator mavenPluginsConfigurator, InitializersExecutor initializersExecutor, + PostJobsExecutor postJobsExecutor, SensorsExecutor sensorsExecutor, + PersistenceManager persistenceManager, SensorContext sensorContext, DefaultIndex index, + EventBus eventBus, UpdateStatusJob updateStatusJob, ProjectInitializer pi, + ScanPersister[] persisters, FileSystemLogger fsLogger, JsonReport jsonReport) { this.phases = phases; this.decoratorsExecutor = decoratorsExecutor; this.mavenPhaseExecutor = mavenPhaseExecutor; @@ -88,12 +88,12 @@ public final class PhaseExecutor { } public PhaseExecutor(Phases phases, DecoratorsExecutor decoratorsExecutor, MavenPhaseExecutor mavenPhaseExecutor, - MavenPluginsConfigurator mavenPluginsConfigurator, InitializersExecutor initializersExecutor, - PostJobsExecutor postJobsExecutor, SensorsExecutor sensorsExecutor, - PersistenceManager persistenceManager, SensorContext sensorContext, DefaultIndex index, - EventBus eventBus, ProjectInitializer pi, ScanPersister[] persisters, FileSystemLogger fsLogger, JsonReport jsonReport) { + MavenPluginsConfigurator mavenPluginsConfigurator, InitializersExecutor initializersExecutor, + PostJobsExecutor postJobsExecutor, SensorsExecutor sensorsExecutor, + PersistenceManager persistenceManager, SensorContext sensorContext, DefaultIndex index, + EventBus eventBus, ProjectInitializer pi, ScanPersister[] persisters, FileSystemLogger fsLogger, JsonReport jsonReport) { this(phases, decoratorsExecutor, mavenPhaseExecutor, mavenPluginsConfigurator, initializersExecutor, postJobsExecutor, - sensorsExecutor, persistenceManager, sensorContext, index, eventBus, null, pi, persisters, fsLogger, jsonReport); + sensorsExecutor, persistenceManager, sensorContext, index, eventBus, null, pi, persisters, fsLogger, jsonReport); } /** diff --git a/sonar-batch/src/main/java/org/sonar/batch/phases/ProjectInitializer.java b/sonar-batch/src/main/java/org/sonar/batch/phases/ProjectInitializer.java index 4ade22a2e68..e4d76a7c899 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/phases/ProjectInitializer.java +++ b/sonar-batch/src/main/java/org/sonar/batch/phases/ProjectInitializer.java @@ -20,43 +20,40 @@ package org.sonar.batch.phases; import org.sonar.api.BatchComponent; +import org.sonar.api.CoreProperties; +import org.sonar.api.config.Settings; import org.sonar.api.resources.Language; import org.sonar.api.resources.Languages; import org.sonar.api.resources.Project; import org.sonar.api.utils.SonarException; -import org.sonar.core.resource.ResourceDao; -import org.sonar.core.resource.ResourceDto; /** * Should be dropped when org.sonar.api.resources.Project is fully refactored. */ public class ProjectInitializer implements BatchComponent { - private ResourceDao resourceDao; private Languages languages; + private Settings settings; - public ProjectInitializer(ResourceDao resourceDao, Languages languages) { - this.resourceDao = resourceDao; + public ProjectInitializer(Settings settings, Languages languages) { + this.settings = settings; this.languages = languages; } public void execute(Project project) { if (project.getLanguage() == null) { - initLanguage(project); + initDeprecatedLanguage(project); } } - private void initLanguage(Project project) { - Language language = languages.get(project.getLanguageKey()); - if (language == null) { - throw new SonarException("Language with key '" + project.getLanguageKey() + "' not found"); + private void initDeprecatedLanguage(Project project) { + String languageKey = settings.getString(CoreProperties.PROJECT_LANGUAGE_PROPERTY); + if (languageKey != null) { + Language language = languages.get(languageKey); + if (language == null) { + throw new SonarException("Language with key '" + languageKey + "' not found"); + } + project.setLanguage(language); } - project.setLanguage(language); - if (project.getId() != null) { - ResourceDto dto = resourceDao.getResource(project.getId()); - dto.setLanguage(project.getLanguageKey()); - resourceDao.insertOrUpdate(dto); - } - } } diff --git a/sonar-batch/src/main/java/org/sonar/batch/phases/SensorsExecutor.java b/sonar-batch/src/main/java/org/sonar/batch/phases/SensorsExecutor.java index d66e08ad0a7..117300ade10 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/phases/SensorsExecutor.java +++ b/sonar-batch/src/main/java/org/sonar/batch/phases/SensorsExecutor.java @@ -23,16 +23,19 @@ import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.BatchComponent; +import org.sonar.api.CoreProperties; 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.database.DatabaseSession; +import org.sonar.api.resources.Language; import org.sonar.api.resources.Project; import org.sonar.api.utils.TimeProfiler; import org.sonar.batch.bootstrap.BatchExtensionDictionnary; import org.sonar.batch.events.EventBus; import org.sonar.batch.scan.filesystem.DefaultModuleFileSystem; +import org.sonar.batch.scan.language.ModuleLanguages; import org.sonar.batch.scan.maven.MavenPluginExecutor; import java.util.Collection; @@ -42,50 +45,70 @@ public class SensorsExecutor implements BatchComponent { private MavenPluginExecutor mavenExecutor; private EventBus eventBus; - private Project project; + private Project module; private DefaultModuleFileSystem fs; private BatchExtensionDictionnary selector; private final DatabaseSession session; private final SensorMatcher sensorMatcher; - private final FileIndexer fileIndexer; + private final ModuleLanguages moduleLanguages; public SensorsExecutor(BatchExtensionDictionnary selector, Project project, DefaultModuleFileSystem fs, MavenPluginExecutor mavenExecutor, EventBus eventBus, - DatabaseSession session, SensorMatcher sensorMatcher, FileIndexer fileIndexer) { + DatabaseSession session, SensorMatcher sensorMatcher, ModuleLanguages moduleLanguages) { this.selector = selector; this.mavenExecutor = mavenExecutor; this.eventBus = eventBus; - this.project = project; + this.module = project; this.fs = fs; this.session = session; this.sensorMatcher = sensorMatcher; - this.fileIndexer = fileIndexer; + this.moduleLanguages = moduleLanguages; } public void execute(SensorContext context) { - Collection<Sensor> sensors = selector.select(Sensor.class, project, true, sensorMatcher); + Collection<Sensor> sensors = selector.select(Sensor.class, module, true, sensorMatcher); eventBus.fireEvent(new SensorsPhaseEvent(Lists.newArrayList(sensors), true)); - fileIndexer.execute(); - for (Sensor sensor : sensors) { // SONAR-2965 In case the sensor takes too much time we close the session to not face a timeout session.commitAndClose(); - eventBus.fireEvent(new SensorExecutionEvent(sensor, true)); - executeMavenPlugin(sensor); - sensor.analyse(project, context); - eventBus.fireEvent(new SensorExecutionEvent(sensor, false)); + if (sensor.shouldExecuteOnProject(module)) { + executeSensor(context, sensor); + } else { + // For backward compatibility try to execute Sensor for each language until it is executed once (or never) + String oldLanguageKey = module.getLanguageKey(); + Language oldLanguage = module.getLanguage(); + for (Language language : moduleLanguages.getModuleLanguages()) { + module.setLanguage(language); + module.getConfiguration().setProperty(CoreProperties.PROJECT_LANGUAGE_PROPERTY, language.getKey()); + if (sensor.shouldExecuteOnProject(module)) { + LOG.warn("Sensor {} should be updated to not depends on deprecated Project::getLanguage or Project::getLanguageKey", sensor); + executeSensor(context, sensor); + break; + } + } + // Restore module language + module.setLanguage(oldLanguage); + module.getConfiguration().setProperty(CoreProperties.PROJECT_LANGUAGE_PROPERTY, oldLanguageKey); + } } eventBus.fireEvent(new SensorsPhaseEvent(Lists.newArrayList(sensors), false)); } + private void executeSensor(SensorContext context, Sensor sensor) { + eventBus.fireEvent(new SensorExecutionEvent(sensor, true)); + executeMavenPlugin(sensor); + sensor.analyse(module, context); + eventBus.fireEvent(new SensorExecutionEvent(sensor, false)); + } + private void executeMavenPlugin(Sensor sensor) { if (sensor instanceof DependsUponMavenPlugin) { - MavenPluginHandler handler = ((DependsUponMavenPlugin) sensor).getMavenPluginHandler(project); + MavenPluginHandler handler = ((DependsUponMavenPlugin) sensor).getMavenPluginHandler(module); if (handler != null) { TimeProfiler profiler = new TimeProfiler(LOG).start("Execute maven plugin " + handler.getArtifactId()); - mavenExecutor.execute(project, fs, handler); + mavenExecutor.execute(module, fs, handler); profiler.stop(); } } 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 ab083e6389e..e1c1f84e39f 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 @@ -19,6 +19,8 @@ */ package org.sonar.batch.scan; +import org.sonar.batch.scan.filesystem.ComponentIndexer; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.BatchExtension; @@ -28,9 +30,11 @@ import org.sonar.api.platform.ComponentContainer; import org.sonar.api.resources.Languages; import org.sonar.api.resources.Project; import org.sonar.api.scan.filesystem.FileExclusions; +import org.sonar.batch.DefaultProfileLoader; import org.sonar.batch.DefaultProjectClasspath; import org.sonar.batch.DefaultSensorContext; import org.sonar.batch.DefaultTimeMachine; +import org.sonar.batch.ProfileLoader; import org.sonar.batch.ProfileProvider; import org.sonar.batch.ProjectTree; import org.sonar.batch.ResourceFilters; @@ -46,7 +50,6 @@ import org.sonar.batch.index.ResourcePersister; import org.sonar.batch.issue.IssuableFactory; import org.sonar.batch.issue.IssueFilters; import org.sonar.batch.issue.ModuleIssues; -import org.sonar.batch.phases.FileIndexer; import org.sonar.batch.phases.PhaseExecutor; import org.sonar.batch.phases.PhasesTimeProfiler; import org.sonar.batch.scan.filesystem.DefaultModuleFileSystem; @@ -59,6 +62,7 @@ import org.sonar.batch.scan.filesystem.LanguageRecognizer; import org.sonar.batch.scan.filesystem.ModuleFileSystemInitializer; import org.sonar.batch.scan.filesystem.ProjectFileSystemAdapter; import org.sonar.batch.scan.filesystem.RemoteFileHashes; +import org.sonar.batch.scan.language.ModuleLanguages; import org.sonar.batch.scan.report.ComponentSelectorFactory; import org.sonar.batch.scan.report.JsonReport; import org.sonar.core.component.ScanPerspectives; @@ -91,6 +95,11 @@ public class ModuleScanContainer extends ComponentContainer { // hack to initialize commons-configuration before ExtensionProviders getComponentByType(ModuleSettings.class); + // Don't override ProfileLoader provided by views plugin in parent container + if (getComponentByType(ProfileLoader.class) == null) { + add(DefaultProfileLoader.class); + } + add( EventBus.class, PhaseExecutor.class, @@ -109,7 +118,8 @@ public class ModuleScanContainer extends ComponentContainer { FileHashes.class, RemoteFileHashes.class, FileIndex.class, - FileIndexer.class, + ComponentIndexer.class, + ModuleLanguages.class, LanguageRecognizer.class, FileSystemLogger.class, DefaultProjectClasspath.class, diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/ScanTask.java b/sonar-batch/src/main/java/org/sonar/batch/scan/ScanTask.java index 4438112c1d8..eec4c01995c 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/scan/ScanTask.java +++ b/sonar-batch/src/main/java/org/sonar/batch/scan/ScanTask.java @@ -23,17 +23,16 @@ import org.sonar.api.CoreProperties; import org.sonar.api.platform.ComponentContainer; import org.sonar.api.task.Task; import org.sonar.api.task.TaskDefinition; -import org.sonar.batch.DefaultProfileLoader; import org.sonar.batch.DefaultProjectTree; import org.sonar.batch.bootstrap.TaskContainer; import org.sonar.batch.phases.Phases; public class ScanTask implements Task { public static final TaskDefinition DEFINITION = TaskDefinition.builder() - .description("Scan project") - .key(CoreProperties.SCAN_TASK) - .taskClass(ScanTask.class) - .build(); + .description("Scan project") + .key(CoreProperties.SCAN_TASK) + .taskClass(ScanTask.class) + .build(); private final ComponentContainer taskContainer; @@ -48,13 +47,12 @@ public class ScanTask implements Task { // Add components specific to project scan (views will use different ones) void scan(ComponentContainer scanContainer) { scanContainer.add( - new Phases().enable(Phases.Phase.values()), - DefaultProjectTree.class, - ProjectExclusions.class, - ProjectReactorValidator.class, - ProjectReactorReady.class, - DefaultProfileLoader.class, - DefaultSensorMatcher.class); + new Phases().enable(Phases.Phase.values()), + DefaultProjectTree.class, + ProjectExclusions.class, + ProjectReactorValidator.class, + ProjectReactorReady.class, + DefaultSensorMatcher.class); scanContainer.execute(); } } diff --git a/sonar-batch/src/main/java/org/sonar/batch/phases/FileIndexer.java b/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/ComponentIndexer.java index 880f74d44e0..ac0bfad02fe 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/phases/FileIndexer.java +++ b/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/ComponentIndexer.java @@ -17,10 +17,11 @@ * 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.phases; +package org.sonar.batch.scan.filesystem; import com.google.common.base.CharMatcher; import com.google.common.io.Files; +import org.apache.commons.lang.StringUtils; import org.sonar.api.BatchComponent; import org.sonar.api.CoreProperties; import org.sonar.api.batch.InstantiationStrategy; @@ -36,32 +37,37 @@ import org.sonar.api.scan.filesystem.FileQuery; import org.sonar.api.scan.filesystem.internal.InputFile; import org.sonar.api.utils.SonarException; import org.sonar.batch.index.ResourceKeyMigration; -import org.sonar.batch.scan.filesystem.DefaultModuleFileSystem; +import org.sonar.batch.scan.language.ModuleLanguages; +import org.sonar.core.resource.ResourceDao; +import org.sonar.core.resource.ResourceDto; /** * Index all files/directories of the module in SQ database and importing source code. * @since 4.2 */ @InstantiationStrategy(InstantiationStrategy.PER_PROJECT) -public class FileIndexer implements BatchComponent { +public class ComponentIndexer implements BatchComponent { - private final DefaultModuleFileSystem fs; private final Languages languages; private final Settings settings; private final SonarIndex sonarIndex; - private ResourceKeyMigration migration; - private Project module; + private final ResourceKeyMigration migration; + private final Project module; + private final ModuleLanguages moduleLanguages; + private final ResourceDao resourceDao; - public FileIndexer(Project module, DefaultModuleFileSystem fs, Languages languages, SonarIndex sonarIndex, Settings settings, ResourceKeyMigration migration) { + public ComponentIndexer(Project module, Languages languages, SonarIndex sonarIndex, Settings settings, ResourceKeyMigration migration, + ModuleLanguages moduleLanguages, ResourceDao resourceDao) { this.module = module; - this.fs = fs; this.languages = languages; this.sonarIndex = sonarIndex; this.settings = settings; this.migration = migration; + this.moduleLanguages = moduleLanguages; + this.resourceDao = resourceDao; } - public void execute() { + public void execute(DefaultModuleFileSystem fs) { boolean importSource = settings.getBoolean(CoreProperties.CORE_IMPORT_SOURCES_PROPERTY); Iterable<InputFile> inputFiles = fs.inputFiles(FileQuery.all()); migration.migrateIfNeeded(module, inputFiles); @@ -75,19 +81,40 @@ public class FileIndexer implements BatchComponent { sonarFile = File.create(inputFile.path(), inputFile.attribute(InputFile.ATTRIBUTE_SOURCE_RELATIVE_PATH), languages.get(languageKey), unitTest); } if (sonarFile != null) { + moduleLanguages.addLanguage(languageKey); sonarIndex.index(sonarFile); - try { - if (importSource) { - String source = Files.toString(inputFile.file(), inputFile.encoding()); - // SONAR-3860 Remove BOM character from source - source = CharMatcher.anyOf("\uFEFF").removeFrom(source); - sonarIndex.setSource(sonarFile, source); - } - } catch (Exception e) { - throw new SonarException("Unable to read and import the source file : '" + inputFile.absolutePath() + "' with the charset : '" - + inputFile.encoding() + "'.", e); + if (importSource) { + importSources(inputFile, sonarFile); } } } + + updateModuleLanguage(); + } + + private void importSources(InputFile inputFile, Resource sonarFile) { + try { + String source = Files.toString(inputFile.file(), inputFile.encoding()); + // SONAR-3860 Remove BOM character from source + source = CharMatcher.anyOf("\uFEFF").removeFrom(source); + sonarIndex.setSource(sonarFile, source); + } catch (Exception e) { + throw new SonarException("Unable to read and import the source file : '" + inputFile.absolutePath() + "' with the charset : '" + + inputFile.encoding() + "'.", e); + } + } + + private void updateModuleLanguage() { + if (module.getId() != null) { + ResourceDto dto = resourceDao.getResource(module.getId()); + if (moduleLanguages.getModuleLanguageKeys().size() == 1) { + dto.setLanguage(moduleLanguages.getModuleLanguageKeys().iterator().next()); + } else if (moduleLanguages.getModuleLanguageKeys().size() > 1) { + dto.setLanguage(StringUtils.join(moduleLanguages.getModuleLanguageKeys(), ",")); + } else { + dto.setLanguage("none"); + } + resourceDao.insertOrUpdate(dto); + } } } diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystem.java b/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystem.java index 4367b8aedd8..80bc58ad5eb 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystem.java +++ b/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystem.java @@ -23,7 +23,6 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import org.apache.commons.lang.StringUtils; -import org.picocontainer.Startable; import org.sonar.api.CoreProperties; import org.sonar.api.config.Settings; import org.sonar.api.resources.Project; @@ -31,6 +30,7 @@ import org.sonar.api.scan.filesystem.FileQuery; import org.sonar.api.scan.filesystem.ModuleFileSystem; import org.sonar.api.scan.filesystem.internal.InputFile; import org.sonar.api.scan.filesystem.internal.InputFiles; +import org.sonar.api.utils.SonarException; import org.sonar.batch.bootstrap.AnalysisMode; import javax.annotation.CheckForNull; @@ -44,7 +44,7 @@ import java.util.List; * * @since 3.5 */ -public class DefaultModuleFileSystem implements ModuleFileSystem, Startable { +public class DefaultModuleFileSystem implements ModuleFileSystem { private final String moduleKey; private final FileIndex index; @@ -57,9 +57,12 @@ public class DefaultModuleFileSystem implements ModuleFileSystem, Startable { private List<File> sourceFiles = Lists.newArrayList(); private List<File> testFiles = Lists.newArrayList(); private AnalysisMode analysisMode; - private boolean dirsChanged = false; + private ComponentIndexer componentIndexer; + private boolean indexed; - public DefaultModuleFileSystem(Project module, Settings settings, FileIndex index, ModuleFileSystemInitializer initializer, AnalysisMode analysisMode) { + public DefaultModuleFileSystem(Project module, Settings settings, FileIndex index, ModuleFileSystemInitializer initializer, AnalysisMode analysisMode, + ComponentIndexer componentIndexer) { + this.componentIndexer = componentIndexer; this.moduleKey = module.getKey(); this.settings = settings; this.index = index; @@ -123,8 +126,7 @@ public class DefaultModuleFileSystem implements ModuleFileSystem, Startable { */ @Deprecated void addSourceDir(File dir) { - sourceDirs.add(dir); - dirsChanged = true; + throw new UnsupportedOperationException("Modifications of the file system are not permitted"); } /** @@ -133,8 +135,7 @@ public class DefaultModuleFileSystem implements ModuleFileSystem, Startable { */ @Deprecated void addTestDir(File dir) { - testDirs.add(dir); - dirsChanged = true; + throw new UnsupportedOperationException("Modifications of the file system are not permitted"); } @Override @@ -157,10 +158,6 @@ public class DefaultModuleFileSystem implements ModuleFileSystem, Startable { * @since 4.0 */ public Iterable<InputFile> inputFiles(FileQuery query) { - if (dirsChanged) { - index(); - dirsChanged = false; - } List<InputFile> result = Lists.newArrayList(); FileQueryFilter filter = new FileQueryFilter(analysisMode, query); for (InputFile input : index.inputFiles(moduleKey)) { @@ -176,16 +173,6 @@ public class DefaultModuleFileSystem implements ModuleFileSystem, Startable { return InputFiles.toFiles(inputFiles(query)); } - @Override - public void start() { - index(); - } - - @Override - public void stop() { - // nothing to do - } - public void resetDirs(File basedir, File buildDir, List<File> sourceDirs, List<File> testDirs, List<File> binaryDirs) { Preconditions.checkNotNull(basedir, "Basedir can't be null"); this.baseDir = basedir; @@ -193,11 +180,15 @@ public class DefaultModuleFileSystem implements ModuleFileSystem, Startable { this.sourceDirs = existingDirs(sourceDirs); this.testDirs = existingDirs(testDirs); this.binaryDirs = existingDirs(binaryDirs); - index(); } public void index() { + if (indexed) { + throw new SonarException("Module filesystem can only be indexed once"); + } + indexed = true; index.index(this); + componentIndexer.execute(this); } private List<File> existingDirs(List<File> dirs) { diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/FileIndex.java b/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/FileIndex.java index d1ffbd92605..34d117be2ee 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/FileIndex.java +++ b/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/FileIndex.java @@ -120,6 +120,7 @@ public class FileIndex implements BatchComponent { } logger.info(String.format("%d files indexed", progress.count)); + } private void indexFiles(DefaultModuleFileSystem fileSystem, Progress progress, List<File> sourceDirs, List<File> sourceFiles, String type) { @@ -161,14 +162,9 @@ public class FileIndex implements BatchComponent { @CheckForNull private InputFile newInputFile(ModuleFileSystem fileSystem, File sourceDir, String type, File file, String path) { - String lang = languageRecognizer.of(file); - if (lang == null) { - return null; - } Map<String, String> attributes = Maps.newHashMap(); set(attributes, InputFile.ATTRIBUTE_TYPE, type); - set(attributes, InputFile.ATTRIBUTE_LANGUAGE, lang); // paths set(attributes, InputFile.ATTRIBUTE_SOURCEDIR_PATH, PathUtils.canonicalPath(sourceDir)); @@ -177,16 +173,22 @@ public class FileIndex implements BatchComponent { String resourceKey = PathUtils.sanitize(path); set(attributes, DefaultInputFile.ATTRIBUTE_COMPONENT_KEY, project.getEffectiveKey() + ":" + resourceKey); + // hash + status + initStatus(file, fileSystem.sourceCharset(), path, attributes); + + DefaultInputFile inputFile = DefaultInputFile.create(file, fileSystem.sourceCharset(), path, attributes); + String lang = languageRecognizer.of(inputFile); + if (lang == null) { + return null; + } + set(inputFile.attributes(), InputFile.ATTRIBUTE_LANGUAGE, lang); if (Java.KEY.equals(lang)) { - set(attributes, DefaultInputFile.ATTRIBUTE_COMPONENT_DEPRECATED_KEY, project.getEffectiveKey() + ":" + set(inputFile.attributes(), DefaultInputFile.ATTRIBUTE_COMPONENT_DEPRECATED_KEY, project.getEffectiveKey() + ":" + JavaFile.fromRelativePath(sourceRelativePath, false).getDeprecatedKey()); } else { - set(attributes, DefaultInputFile.ATTRIBUTE_COMPONENT_DEPRECATED_KEY, project.getEffectiveKey() + ":" + sourceRelativePath); + set(inputFile.attributes(), DefaultInputFile.ATTRIBUTE_COMPONENT_DEPRECATED_KEY, project.getEffectiveKey() + ":" + sourceRelativePath); } - // hash + status - initStatus(file, fileSystem.sourceCharset(), path, attributes); - - return DefaultInputFile.create(file, fileSystem.sourceCharset(), path, attributes); + return inputFile; } private void initStatus(File file, Charset charset, String baseRelativePath, Map<String, String> attributes) { diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/LanguageRecognizer.java b/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/LanguageRecognizer.java index fa8269afaad..26d898ff2ea 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/LanguageRecognizer.java +++ b/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/LanguageRecognizer.java @@ -20,79 +20,126 @@ package org.sonar.batch.scan.filesystem; import com.google.common.collect.HashMultimap; +import com.google.common.collect.Maps; import com.google.common.collect.SetMultimap; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; import org.picocontainer.Startable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.sonar.api.BatchComponent; +import org.sonar.api.CoreProperties; +import org.sonar.api.config.Settings; import org.sonar.api.resources.Language; -import org.sonar.api.resources.Project; +import org.sonar.api.resources.Languages; +import org.sonar.api.scan.filesystem.internal.InputFile; +import org.sonar.api.utils.SonarException; import javax.annotation.CheckForNull; -import java.io.File; + +import java.util.Map; import java.util.Set; /** - * Detect language of source files. Simplistic, based on file extensions. + * Detect language of source files. */ public class LanguageRecognizer implements BatchComponent, Startable { - private final Project project; - private final Language[] languages; + private static final Logger LOG = LoggerFactory.getLogger(LanguageRecognizer.class); + + private final Languages languages; /** * Lower-case extension -> languages */ private SetMultimap<String, String> langsByExtension = HashMultimap.create(); + private Map<String, PathPattern[]> patternByLanguage = Maps.newLinkedHashMap(); - /** - * Some plugins, like web and cobol, can analyze all the source files, whatever - * their file extension. This behavior is kept for backward-compatibility, - * but it should be fixed with future multi-language support. - */ - private boolean ignoreFileExtension = false; + private Settings settings; - public LanguageRecognizer(Project project, Language[] languages) { - this.project = project; + public LanguageRecognizer(Settings settings, Languages languages) { + this.settings = settings; this.languages = languages; } - /** - * When no language plugin is installed - */ - public LanguageRecognizer(Project project) { - this(project, new Language[0]); - } - @Override public void start() { - for (Language language : languages) { - if (language.getFileSuffixes().length == 0 && language.getKey().equals(project.getLanguageKey())) { - ignoreFileExtension = true; - - } else { - for (String suffix : language.getFileSuffixes()) { - String extension = sanitizeExtension(suffix); - langsByExtension.put(extension, language.getKey()); - } + for (Language language : languages.all()) { + for (String suffix : language.getFileSuffixes()) { + String extension = sanitizeExtension(suffix); + langsByExtension.put(extension, language.getKey()); + } + String[] filePatterns = settings.getStringArray(getFilePatternPropKey(language.getKey())); + PathPattern[] pathPatterns = PathPattern.create(filePatterns); + if (pathPatterns.length > 0) { + patternByLanguage.put(language.getKey(), pathPatterns); } } } + private String getFilePatternPropKey(String languageKey) { + return "sonar." + languageKey + ".filePatterns"; + } + @Override public void stop() { // do nothing } @CheckForNull - String of(File file) { - if (ignoreFileExtension) { - return project.getLanguageKey(); + String of(InputFile inputFile) { + // First try with patterns + String forcedLanguage = null; + for (Map.Entry<String, PathPattern[]> languagePattern : patternByLanguage.entrySet()) { + PathPattern[] patterns = languagePattern.getValue(); + for (PathPattern pathPattern : patterns) { + if (pathPattern.match(inputFile)) { + if (forcedLanguage == null) { + forcedLanguage = languagePattern.getKey(); + break; + } else { + // Language was already forced by another pattern + throw new SonarException("Language of file '" + inputFile.path() + "' can not be decided as the file matches patterns of both " + getFilePatternPropKey(forcedLanguage) + + " and " + + getFilePatternPropKey(languagePattern.getKey())); + } + } + } + } + if (forcedLanguage != null) { + LOG.debug("Language of file '" + inputFile.path() + "' was forced to '" + forcedLanguage + "'"); + return forcedLanguage; + } + + String extension = sanitizeExtension(FilenameUtils.getExtension(inputFile.file().getName())); + + // Check if deprecated sonar.language is used + String languageKey = settings.getString(CoreProperties.PROJECT_LANGUAGE_PROPERTY); + if (StringUtils.isNotBlank(languageKey)) { + Language language = languages.get(languageKey); + if (language == null) { + throw new SonarException("No language is installed with key '" + languageKey + "'. Please update property '" + CoreProperties.PROJECT_LANGUAGE_PROPERTY + "'"); + } + // Languages without declared suffixes match everything + String[] fileSuffixes = language.getFileSuffixes(); + if (fileSuffixes.length == 0) { + return languageKey; + } + for (String fileSuffix : fileSuffixes) { + if (sanitizeExtension(fileSuffix).equals(extension)) { + return languageKey; + } + } + return null; } - // multi-language is not supported yet. Filter on project language - String extension = sanitizeExtension(FilenameUtils.getExtension(file.getName())); + + // At this point use extension to detect language Set<String> langs = langsByExtension.get(extension); - return langs.contains(project.getLanguageKey()) ? project.getLanguageKey() : null; + if (langs.size() > 1) { + throw new SonarException("Language of file '" + inputFile.path() + "' can not be decided as the file extension '" + extension + "' is declared by several languages: " + + StringUtils.join(langs, ", ")); + } + return langs.isEmpty() ? null : langs.iterator().next(); } static String sanitizeExtension(String suffix) { diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/PathPattern.java b/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/PathPattern.java index 446052c84c5..650f447e476 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/PathPattern.java +++ b/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/PathPattern.java @@ -81,7 +81,7 @@ abstract class PathPattern { } /** - * Path relative to source directory + * Path relative to module basedir */ private static class RelativePathPattern extends PathPattern { private RelativePathPattern(String pattern) { @@ -90,7 +90,7 @@ abstract class PathPattern { @Override boolean match(InputFile inputFile) { - String path = inputFile.attribute(InputFile.ATTRIBUTE_SOURCE_RELATIVE_PATH); + String path = inputFile.path(); return path != null && pattern.match(path); } diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/language/ModuleLanguages.java b/sonar-batch/src/main/java/org/sonar/batch/scan/language/ModuleLanguages.java new file mode 100644 index 00000000000..fa986815858 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/scan/language/ModuleLanguages.java @@ -0,0 +1,71 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 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.scan.language; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.BatchExtension; +import org.sonar.api.CoreProperties; +import org.sonar.api.config.Settings; +import org.sonar.api.resources.Language; +import org.sonar.api.resources.Languages; +import org.sonar.api.utils.SonarException; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Give access to all languages detected on the current module + * @since 4.2 + */ +public class ModuleLanguages implements BatchExtension { + + private static final Logger LOG = LoggerFactory.getLogger(ModuleLanguages.class); + + private Map<String, Language> moduleLanguages = new HashMap<String, Language>(); + + private Languages languages; + + public ModuleLanguages(Settings settings, Languages languages) { + this.languages = languages; + if (settings.hasKey(CoreProperties.PROJECT_LANGUAGE_PROPERTY)) { + String languageKey = settings.getString(CoreProperties.PROJECT_LANGUAGE_PROPERTY); + LOG.info("Language is forced to {}", languageKey); + addLanguage(languageKey); + } + } + + public void addLanguage(String languageKey) { + Language language = languages.get(languageKey); + if (language == null) { + throw new SonarException("Unknow language " + languageKey); + } + moduleLanguages.put(languageKey, language); + } + + public Collection<String> getModuleLanguageKeys() { + return moduleLanguages.keySet(); + } + + public Collection<Language> getModuleLanguages() { + return moduleLanguages.values(); + } +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/DefaultProfileLoaderTest.java b/sonar-batch/src/test/java/org/sonar/batch/DefaultProfileLoaderTest.java index 693d4d67f09..f29ba9b9a1e 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/DefaultProfileLoaderTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/DefaultProfileLoaderTest.java @@ -25,15 +25,21 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.sonar.api.config.Settings; +import org.sonar.api.profiles.Alert; import org.sonar.api.profiles.RulesProfile; import org.sonar.api.resources.AbstractLanguage; import org.sonar.api.resources.Java; import org.sonar.api.resources.Language; import org.sonar.api.resources.Languages; import org.sonar.api.resources.Project; +import org.sonar.api.rules.ActiveRule; +import org.sonar.api.rules.RulePriority; import org.sonar.api.utils.SonarException; +import org.sonar.batch.scan.language.ModuleLanguages; import org.sonar.jpa.dao.ProfilesDao; +import java.util.Arrays; + import static org.fest.assertions.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -50,50 +56,105 @@ public class DefaultProfileLoaderTest { @Before public void setUp() { dao = mock(ProfilesDao.class); - Language java = new AbstractLanguage("java", "Java") { - public String[] getFileSuffixes() { - return null; - }; - }; - Language cobol = new AbstractLanguage("js", "JavaScript") { + Language cobol = new AbstractLanguage("cobol", "Cobol") { public String[] getFileSuffixes() { return null; }; }; - languages = new Languages(java, cobol); + languages = new Languages(Java.INSTANCE, cobol); } @Test public void should_get_configured_profile() { Settings settings = new Settings(); settings.setProperty("sonar.profile.java", "legacy profile"); + settings.setProperty("sonar.profile.cobol", "cobol profile"); when(dao.getProfile(Java.KEY, "legacy profile")).thenReturn(RulesProfile.create("legacy profile", "java")); + when(dao.getProfile("cobol", "cobol profile")).thenReturn(RulesProfile.create("cobol profile", "cobol")); - RulesProfile profile = new DefaultProfileLoader(dao).load(javaProject, settings); + ModuleLanguages moduleLanguages = new ModuleLanguages(settings, languages); + moduleLanguages.addLanguage("java"); + RulesProfile profile = new DefaultProfileLoader(dao, moduleLanguages, languages).load(javaProject, settings); assertThat(profile.getName()).isEqualTo("legacy profile"); } @Test - public void should_fail_if_not_found() { + public void some_methods_should_support_multilanguage() { Settings settings = new Settings(); - settings.setProperty("sonar.profile.java", "unknown"); + settings.setProperty("sonar.profile.java", "java profile"); + settings.setProperty("sonar.profile.cobol", "cobol profile"); + + RulesProfile javaProfile = RulesProfile.create("java profile", "java"); + org.sonar.api.rules.Rule javaRule = new org.sonar.api.rules.Rule("javaplugin", "javarule"); + ActiveRule javaActiveRule = new ActiveRule(javaProfile, javaRule, RulePriority.BLOCKER); + javaProfile.addActiveRule(javaActiveRule); + Alert javaAlert = mock(Alert.class); + javaProfile.setAlerts(Arrays.asList(javaAlert)); + when(dao.getProfile(Java.KEY, "java profile")).thenReturn(javaProfile); + + RulesProfile cobolProfile = RulesProfile.create("cobol profile", "cobol"); + org.sonar.api.rules.Rule cobolRule = new org.sonar.api.rules.Rule("cobolplugin", "cobolrule"); + ActiveRule cobolActiveRule = new ActiveRule(cobolProfile, cobolRule, RulePriority.BLOCKER); + cobolProfile.addActiveRule(cobolActiveRule); + Alert cobolAlert = mock(Alert.class); + cobolProfile.setAlerts(Arrays.asList(cobolAlert)); + when(dao.getProfile("cobol", "cobol profile")).thenReturn(cobolProfile); + + ModuleLanguages moduleLanguages = new ModuleLanguages(settings, languages); + moduleLanguages.addLanguage("java"); + moduleLanguages.addLanguage("cobol"); + RulesProfile profile = new DefaultProfileLoader(dao, moduleLanguages, languages).load(javaProject, settings); + + assertThat(profile.getActiveRules()).containsOnly(javaActiveRule, cobolActiveRule); + assertThat(profile.getActiveRules(true)).containsOnly(javaActiveRule, cobolActiveRule); + assertThat(profile.getActiveRulesByRepository("javaplugin")).containsOnly(javaActiveRule); + assertThat(profile.getActiveRulesByRepository("cobolplugin")).containsOnly(cobolActiveRule); + assertThat(profile.getActiveRule("javaplugin", "javarule")).isEqualTo(javaActiveRule); + assertThat(profile.getActiveRule(javaRule)).isEqualTo(javaActiveRule); + assertThat(profile.getActiveRule("cobolplugin", "cobolrule")).isEqualTo(cobolActiveRule); + assertThat(profile.getActiveRule(cobolRule)).isEqualTo(cobolActiveRule); + assertThat(profile.getAlerts()).containsOnly(javaAlert, cobolAlert); + // Hack for CommonChecksDecorator + assertThat(profile.getLanguage()).isEqualTo(""); + } + + @Test + public void some_methods_should_not_support_multilanguage() { + Settings settings = new Settings(); + settings.setProperty("sonar.profile.java", "java profile"); + settings.setProperty("sonar.profile.cobol", "cobol profile"); + + RulesProfile javaProfile = RulesProfile.create("java profile", "java"); + org.sonar.api.rules.Rule javaRule = new org.sonar.api.rules.Rule("javaplugin", "javarule"); + ActiveRule javaActiveRule = new ActiveRule(javaProfile, javaRule, RulePriority.BLOCKER); + javaProfile.addActiveRule(javaActiveRule); + when(dao.getProfile(Java.KEY, "java profile")).thenReturn(javaProfile); + + RulesProfile cobolProfile = RulesProfile.create("cobol profile", "cobol"); + org.sonar.api.rules.Rule cobolRule = new org.sonar.api.rules.Rule("cobolplugin", "cobolrule"); + ActiveRule cobolActiveRule = new ActiveRule(cobolProfile, cobolRule, RulePriority.BLOCKER); + cobolProfile.addActiveRule(cobolActiveRule); + when(dao.getProfile("cobol", "cobol profile")).thenReturn(cobolProfile); + + ModuleLanguages moduleLanguages = new ModuleLanguages(settings, languages); + moduleLanguages.addLanguage("java"); + moduleLanguages.addLanguage("cobol"); + RulesProfile profile = new DefaultProfileLoader(dao, moduleLanguages, languages).load(javaProject, settings); thrown.expect(SonarException.class); - thrown.expectMessage("Quality profile not found : unknown, language java"); - new DefaultProfileLoader(dao).load(javaProject, settings); + thrown.expectMessage("Please update your plugin to support multi-language analysis"); + profile.getId(); } - /** - * SONAR-3125 - */ @Test - public void should_give_explicit_message_if_default_profile_not_found() { - Project cobolProject = newProject("cobol"); + public void should_fail_if_not_found() { + Settings settings = new Settings(); + settings.setProperty("sonar.profile.java", "unknown"); thrown.expect(SonarException.class); - thrown.expectMessage("You must install a plugin that supports the language key 'cobol'"); - new DefaultProfileLoader(dao).load(cobolProject, new Settings()); + thrown.expectMessage("Quality profile not found : unknown, language java"); + new DefaultProfileLoader(dao, new ModuleLanguages(settings, languages), languages).load(javaProject, settings); } private Project newProject(String language) { diff --git a/sonar-batch/src/test/java/org/sonar/batch/phases/FileIndexerTest.java b/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/ComponentIndexerTest.java index e029d097752..3b00cc1c9b1 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/phases/FileIndexerTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/ComponentIndexerTest.java @@ -17,7 +17,7 @@ * 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.phases; +package org.sonar.batch.scan.filesystem; import com.google.common.base.Charsets; import org.apache.commons.io.FileUtils; @@ -43,7 +43,8 @@ import org.sonar.api.scan.filesystem.internal.DefaultInputFile; import org.sonar.api.scan.filesystem.internal.InputFile; import org.sonar.api.scan.filesystem.internal.InputFileBuilder; import org.sonar.batch.index.ResourceKeyMigration; -import org.sonar.batch.scan.filesystem.DefaultModuleFileSystem; +import org.sonar.batch.scan.language.ModuleLanguages; +import org.sonar.core.resource.ResourceDao; import java.io.File; import java.io.IOException; @@ -56,7 +57,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -public class FileIndexerTest { +public class ComponentIndexerTest { @Rule public TemporaryFolder temp = new TemporaryFolder(); @@ -95,8 +96,10 @@ public class FileIndexerTest { newInputFile("src/main/java2/foo/bar/Foo.java", "", "foo/bar/Foo.java", "java", false), newInputFile("src/test/java/foo/bar/FooTest.java", "", "foo/bar/FooTest.java", "java", true))); when(project.getLanguageKey()).thenReturn(Java.KEY); - FileIndexer indexer = new FileIndexer(project, fs, new Languages(Java.INSTANCE), sonarIndex, settings, mock(ResourceKeyMigration.class)); - indexer.execute(); + Languages languages = new Languages(Java.INSTANCE); + ComponentIndexer indexer = new ComponentIndexer(project, languages, sonarIndex, settings, mock(ResourceKeyMigration.class), new ModuleLanguages(settings, languages), + mock(ResourceDao.class)); + indexer.execute(fs); verify(sonarIndex).index(JavaFile.create("src/main/java/foo/bar/Foo.java", "foo/bar/Foo.java", false)); verify(sonarIndex).index(JavaFile.create("src/main/java2/foo/bar/Foo.java", "foo/bar/Foo.java", false)); @@ -119,8 +122,10 @@ public class FileIndexerTest { newInputFile("src/test/foo/bar/FooTest.cbl", "", "foo/bar/FooTest.cbl", "cobol", true))); when(project.getLanguageKey()).thenReturn("cobol"); - FileIndexer indexer = new FileIndexer(project, fs, new Languages(cobolLanguage), sonarIndex, settings, mock(ResourceKeyMigration.class)); - indexer.execute(); + Languages languages = new Languages(cobolLanguage); + ComponentIndexer indexer = new ComponentIndexer(project, languages, sonarIndex, settings, mock(ResourceKeyMigration.class), new ModuleLanguages(settings, languages), + mock(ResourceDao.class)); + indexer.execute(fs); verify(sonarIndex).index(org.sonar.api.resources.File.create("/src/foo/bar/Foo.cbl", "foo/bar/Foo.cbl", cobolLanguage, false)); verify(sonarIndex).index(org.sonar.api.resources.File.create("/src2/foo/bar/Foo.cbl", "foo/bar/Foo.cbl", cobolLanguage, false)); @@ -134,8 +139,10 @@ public class FileIndexerTest { when(fs.inputFiles(FileQuery.all())).thenReturn((Iterable) Arrays.asList( newInputFile("src/main/java/foo/bar/Foo.java", "sample code", "foo/bar/Foo.java", "java", false))); when(project.getLanguageKey()).thenReturn(Java.KEY); - FileIndexer indexer = new FileIndexer(project, fs, new Languages(Java.INSTANCE), sonarIndex, settings, mock(ResourceKeyMigration.class)); - indexer.execute(); + Languages languages = new Languages(Java.INSTANCE); + ComponentIndexer indexer = new ComponentIndexer(project, languages, sonarIndex, settings, mock(ResourceKeyMigration.class), new ModuleLanguages(settings, languages), + mock(ResourceDao.class)); + indexer.execute(fs); Resource sonarFile = JavaFile.create("src/main/java/foo/bar/Foo.java", "foo/bar/Foo.java", false); verify(sonarIndex).index(sonarFile); @@ -185,8 +192,10 @@ public class FileIndexerTest { .attribute(InputFile.ATTRIBUTE_LANGUAGE, "java") .build())); when(project.getLanguageKey()).thenReturn(Java.KEY); - FileIndexer indexer = new FileIndexer(project, fs, new Languages(Java.INSTANCE), sonarIndex, settings, mock(ResourceKeyMigration.class)); - indexer.execute(); + Languages languages = new Languages(Java.INSTANCE); + ComponentIndexer indexer = new ComponentIndexer(project, languages, sonarIndex, settings, mock(ResourceKeyMigration.class), new ModuleLanguages(settings, languages), + mock(ResourceDao.class)); + indexer.execute(fs); Resource sonarFile = JavaFile.create("src/main/java/foo/bar/Foo.java", "foo/bar/Foo.java", false); @@ -212,8 +221,10 @@ public class FileIndexerTest { .attribute(InputFile.ATTRIBUTE_LANGUAGE, "java") .build())); when(project.getLanguageKey()).thenReturn(Java.KEY); - FileIndexer indexer = new FileIndexer(project, fs, new Languages(Java.INSTANCE), sonarIndex, settings, mock(ResourceKeyMigration.class)); - indexer.execute(); + Languages languages = new Languages(Java.INSTANCE); + ComponentIndexer indexer = new ComponentIndexer(project, languages, sonarIndex, settings, mock(ResourceKeyMigration.class), new ModuleLanguages(settings, languages), + mock(ResourceDao.class)); + indexer.execute(fs); Resource sonarFile = JavaFile.create("/src/main/java/foo/bar/Foo.java", "foo/bar/Foo.java", false); diff --git a/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystemTest.java b/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystemTest.java index 6c0dbbe079a..ef1298b4514 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystemTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystemTest.java @@ -60,18 +60,20 @@ public class DefaultModuleFileSystemTest { FileIndex fileIndex = mock(FileIndex.class); ModuleFileSystemInitializer initializer = mock(ModuleFileSystemInitializer.class, Mockito.RETURNS_DEEP_STUBS); private AnalysisMode mode; + private ComponentIndexer componentIndexer; @Before public void before() { mode = mock(AnalysisMode.class); + componentIndexer = mock(ComponentIndexer.class); } @Test public void test_equals_and_hashCode() throws Exception { - DefaultModuleFileSystem foo1 = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode); - DefaultModuleFileSystem foo2 = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode); - DefaultModuleFileSystem bar = new DefaultModuleFileSystem(new Project("bar"), settings, fileIndex, initializer, mode); - DefaultModuleFileSystem branch = new DefaultModuleFileSystem(new Project("bar", "branch", "My project"), settings, fileIndex, initializer, mode); + DefaultModuleFileSystem foo1 = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode, componentIndexer); + DefaultModuleFileSystem foo2 = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode, componentIndexer); + DefaultModuleFileSystem bar = new DefaultModuleFileSystem(new Project("bar"), settings, fileIndex, initializer, mode, componentIndexer); + DefaultModuleFileSystem branch = new DefaultModuleFileSystem(new Project("bar", "branch", "My project"), settings, fileIndex, initializer, mode, componentIndexer); assertThat(foo1.moduleKey()).isEqualTo("foo"); assertThat(branch.moduleKey()).isEqualTo("bar:branch"); @@ -85,7 +87,7 @@ public class DefaultModuleFileSystemTest { @Test public void default_source_encoding() { - DefaultModuleFileSystem fs = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode); + DefaultModuleFileSystem fs = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode, componentIndexer); assertThat(fs.sourceCharset()).isEqualTo(Charset.defaultCharset()); assertThat(fs.isDefaultSourceCharset()).isTrue(); @@ -94,7 +96,7 @@ public class DefaultModuleFileSystemTest { @Test public void source_encoding_is_set() { settings.setProperty(CoreProperties.ENCODING_PROPERTY, "Cp1124"); - DefaultModuleFileSystem fs = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode); + DefaultModuleFileSystem fs = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode, componentIndexer); assertThat(fs.sourceCharset()).isEqualTo(Charset.forName("Cp1124")); @@ -118,7 +120,7 @@ public class DefaultModuleFileSystemTest { when(initializer.additionalSourceFiles()).thenReturn(Arrays.asList(additionalFile)); when(initializer.additionalTestFiles()).thenReturn(Arrays.asList(additionalTest)); - DefaultModuleFileSystem fs = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode); + DefaultModuleFileSystem fs = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode, componentIndexer); assertThat(fs.baseDir().getCanonicalPath()).isEqualTo(basedir.getCanonicalPath()); assertThat(fs.workingDir().getCanonicalPath()).isEqualTo(workingDir.getCanonicalPath()); @@ -137,7 +139,7 @@ public class DefaultModuleFileSystemTest { when(initializer.workingDir()).thenReturn(basedir); when(initializer.sourceDirs()).thenReturn(Arrays.asList(new File(basedir, "src/main/java"))); - DefaultModuleFileSystem fs = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode); + DefaultModuleFileSystem fs = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode, componentIndexer); File existingDir = temp.newFolder("new_folder"); File notExistingDir = new File(existingDir, "not_exist"); @@ -153,12 +155,11 @@ public class DefaultModuleFileSystemTest { assertThat(fs.testDirs().get(0).getCanonicalPath()).isEqualTo(existingDir.getCanonicalPath()); assertThat(fs.binaryDirs()).hasSize(1); assertThat(fs.binaryDirs().get(0).getCanonicalPath()).isEqualTo(existingDir.getCanonicalPath()); - verify(fileIndex).index(fs); } @Test public void should_search_input_files() throws Exception { - DefaultModuleFileSystem fs = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode); + DefaultModuleFileSystem fs = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode, componentIndexer); File mainFile = temp.newFile(); InputFile mainInput = DefaultInputFile.create(mainFile, Charsets.UTF_8, "Main.java", ImmutableMap.of(InputFile.ATTRIBUTE_TYPE, InputFile.TYPE_SOURCE)); @@ -175,13 +176,13 @@ public class DefaultModuleFileSystemTest { @Test public void should_index() throws Exception { - DefaultModuleFileSystem fs = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode); + DefaultModuleFileSystem fs = new DefaultModuleFileSystem(new Project("foo"), settings, fileIndex, initializer, mode, componentIndexer); verifyZeroInteractions(fileIndex); - fs.start(); + fs.index(); verify(fileIndex).index(fs); - fs.stop(); + verify(componentIndexer).execute(fs); } } diff --git a/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/LanguageRecognizerTest.java b/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/LanguageRecognizerTest.java index 3903cd11df0..dad6ae8e240 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/LanguageRecognizerTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/LanguageRecognizerTest.java @@ -19,28 +19,35 @@ */ package org.sonar.batch.scan.filesystem; +import com.google.common.base.Charsets; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; -import org.mockito.ArgumentMatcher; +import org.sonar.api.CoreProperties; +import org.sonar.api.config.Settings; import org.sonar.api.resources.Language; -import org.sonar.api.resources.Project; +import org.sonar.api.resources.Languages; +import org.sonar.api.scan.filesystem.internal.InputFile; +import org.sonar.api.scan.filesystem.internal.InputFileBuilder; +import org.sonar.api.utils.SonarException; -import java.util.Set; +import java.io.File; +import java.io.IOException; import static org.fest.assertions.Assertions.assertThat; -import static org.mockito.Matchers.argThat; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; public class LanguageRecognizerTest { @Rule public TemporaryFolder temp = new TemporaryFolder(); + @Rule + public ExpectedException thrown = ExpectedException.none(); + @Test public void test_sanitizeExtension() throws Exception { assertThat(LanguageRecognizer.sanitizeExtension(".cbl")).isEqualTo("cbl"); @@ -51,65 +58,150 @@ public class LanguageRecognizerTest { @Test public void search_by_file_extension() throws Exception { - Language[] languages = new Language[] {new MockLanguage("java", "java", "jav"), new MockLanguage("cobol", "cbl", "cob")}; - LanguageRecognizer recognizer = new LanguageRecognizer(newProject("java"), languages); + Languages languages = new Languages(new MockLanguage("java", "java", "jav"), new MockLanguage("cobol", "cbl", "cob")); + LanguageRecognizer recognizer = new LanguageRecognizer(new Settings(), languages); recognizer.start(); - assertThat(recognizer.of(temp.newFile("Foo.java"))).isEqualTo("java"); - assertThat(recognizer.of(temp.newFile("Foo.JAVA"))).isEqualTo("java"); - assertThat(recognizer.of(temp.newFile("Foo.jav"))).isEqualTo("java"); - assertThat(recognizer.of(temp.newFile("Foo.Jav"))).isEqualTo("java"); - - // multi-language is not supported yet -> filter on project language - assertThat(recognizer.of(temp.newFile("abc.cbl"))).isNull(); - assertThat(recognizer.of(temp.newFile("abc.CBL"))).isNull(); - assertThat(recognizer.of(temp.newFile("abc.php"))).isNull(); - assertThat(recognizer.of(temp.newFile("abc"))).isNull(); + assertThat(recognizer.of(newInputFile("Foo.java"))).isEqualTo("java"); + assertThat(recognizer.of(newInputFile("Foo.JAVA"))).isEqualTo("java"); + assertThat(recognizer.of(newInputFile("Foo.jav"))).isEqualTo("java"); + assertThat(recognizer.of(newInputFile("Foo.Jav"))).isEqualTo("java"); + + assertThat(recognizer.of(newInputFile("abc.cbl"))).isEqualTo("cobol"); + assertThat(recognizer.of(newInputFile("abc.CBL"))).isEqualTo("cobol"); + + assertThat(recognizer.of(newInputFile("abc.php"))).isNull(); + assertThat(recognizer.of(newInputFile("abc"))).isNull(); recognizer.stop(); } @Test public void should_not_fail_if_no_language() throws Exception { - LanguageRecognizer recognizer = spy(new LanguageRecognizer(newProject("java"))); + LanguageRecognizer recognizer = spy(new LanguageRecognizer(new Settings(), new Languages())); recognizer.start(); - assertThat(recognizer.of(temp.newFile("Foo.java"))).isNull(); + assertThat(recognizer.of(newInputFile("Foo.java"))).isNull(); } @Test public void plugin_can_declare_a_file_extension_twice_for_case_sensitivity() throws Exception { - Language[] languages = new Language[] {new MockLanguage("abap", "abap", "ABAP")}; + Languages languages = new Languages(new MockLanguage("abap", "abap", "ABAP")); - LanguageRecognizer recognizer = new LanguageRecognizer(newProject("abap"), languages); + LanguageRecognizer recognizer = new LanguageRecognizer(new Settings(), languages); recognizer.start(); - assertThat(recognizer.of(temp.newFile("abc.abap"))).isEqualTo("abap"); + assertThat(recognizer.of(newInputFile("abc.abap"))).isEqualTo("abap"); } @Test public void language_with_no_extension() throws Exception { // abap does not declare any file extensions. // When analyzing an ABAP project, then all source files must be parsed. - Language[] languages = new Language[] {new MockLanguage("java", "java"), new MockLanguage("abap")}; + Languages languages = new Languages(new MockLanguage("java", "java"), new MockLanguage("abap")); // No side-effect on non-ABAP projects - LanguageRecognizer recognizer = new LanguageRecognizer(newProject("java"), languages); + LanguageRecognizer recognizer = new LanguageRecognizer(new Settings(), languages); + recognizer.start(); + assertThat(recognizer.of(newInputFile("abc"))).isNull(); + assertThat(recognizer.of(newInputFile("abc.abap"))).isNull(); + assertThat(recognizer.of(newInputFile("abc.java"))).isEqualTo("java"); + recognizer.stop(); + + Settings settings = new Settings(); + settings.setProperty(CoreProperties.PROJECT_LANGUAGE_PROPERTY, "abap"); + recognizer = new LanguageRecognizer(settings, languages); + recognizer.start(); + assertThat(recognizer.of(newInputFile("abc"))).isEqualTo("abap"); + assertThat(recognizer.of(newInputFile("abc.txt"))).isEqualTo("abap"); + assertThat(recognizer.of(newInputFile("abc.java"))).isEqualTo("abap"); + recognizer.stop(); + } + + @Test + public void force_language_using_deprecated_property() throws Exception { + Languages languages = new Languages(new MockLanguage("java", "java"), new MockLanguage("php", "php")); + + Settings settings = new Settings(); + settings.setProperty(CoreProperties.PROJECT_LANGUAGE_PROPERTY, "java"); + LanguageRecognizer recognizer = new LanguageRecognizer(settings, languages); + recognizer.start(); + assertThat(recognizer.of(newInputFile("abc"))).isNull(); + assertThat(recognizer.of(newInputFile("abc.php"))).isNull(); + assertThat(recognizer.of(newInputFile("abc.java"))).isEqualTo("java"); + recognizer.stop(); + } + + @Test + public void fail_if_invalid_language() throws Exception { + Languages languages = new Languages(new MockLanguage("java", "java"), new MockLanguage("php", "php")); + + Settings settings = new Settings(); + settings.setProperty(CoreProperties.PROJECT_LANGUAGE_PROPERTY, "unknow"); + LanguageRecognizer recognizer = new LanguageRecognizer(settings, languages); recognizer.start(); - assertThat(recognizer.of(temp.newFile("abc"))).isNull(); - assertThat(recognizer.of(temp.newFile("abc.abap"))).isNull(); - assertThat(recognizer.of(temp.newFile("abc.java"))).isEqualTo("java"); + thrown.expect(SonarException.class); + thrown.expectMessage("No language is installed with key 'unknow'. Please update property 'sonar.language'"); + recognizer.of(newInputFile("abc")); recognizer.stop(); + } + + @Test + public void fail_if_conflicting_language_suffix() throws Exception { + Languages languages = new Languages(new MockLanguage("xml", "xhtml"), new MockLanguage("web", "xhtml")); + + Settings settings = new Settings(); + LanguageRecognizer recognizer = new LanguageRecognizer(settings, languages); + recognizer.start(); + thrown.expect(SonarException.class); + thrown.expectMessage(new BaseMatcher<String>() { + @Override + public void describeTo(Description arg0) { + } + + @Override + public boolean matches(Object arg0) { + // Need custom matcher because order of language in the exception is not deterministic (hashmap) + return arg0.toString().contains("Language of file 'abc.xhtml' can not be decided as the file extension 'xhtml' is declared by several languages: ") + && arg0.toString().contains("web") + && arg0.toString().contains("xml"); + } + }); + recognizer.of(newInputFile("abc.xhtml")); + recognizer.stop(); + } + + @Test + public void solve_conflict_using_filepattern() throws Exception { + Languages languages = new Languages(new MockLanguage("xml", "xhtml"), new MockLanguage("web", "xhtml")); + + Settings settings = new Settings(); + settings.setProperty("sonar.xml.filePatterns", "xml/**"); + settings.setProperty("sonar.web.filePatterns", "web/**"); + LanguageRecognizer recognizer = new LanguageRecognizer(settings, languages); + recognizer.start(); + assertThat(recognizer.of(newInputFile("xml/abc.xhtml"))).isEqualTo("xml"); + assertThat(recognizer.of(newInputFile("web/abc.xhtml"))).isEqualTo("web"); + recognizer.stop(); + } + + @Test + public void fail_if_conflicting_filepattern() throws Exception { + Languages languages = new Languages(new MockLanguage("abap", "abap"), new MockLanguage("cobol", "cobol")); - recognizer = new LanguageRecognizer(newProject("abap"), languages); + Settings settings = new Settings(); + settings.setProperty("sonar.abap.filePatterns", "*.abap,*.txt"); + settings.setProperty("sonar.cobol.filePatterns", "*.cobol,*.txt"); + LanguageRecognizer recognizer = new LanguageRecognizer(settings, languages); recognizer.start(); - assertThat(recognizer.of(temp.newFile("abc"))).isEqualTo("abap"); - assertThat(recognizer.of(temp.newFile("abc.txt"))).isEqualTo("abap"); - assertThat(recognizer.of(temp.newFile("abc.java"))).isEqualTo("abap"); + assertThat(recognizer.of(newInputFile("abc.abap"))).isEqualTo("abap"); + assertThat(recognizer.of(newInputFile("abc.cobol"))).isEqualTo("cobol"); + thrown.expect(SonarException.class); + thrown.expectMessage("Language of file 'abc.txt' can not be decided as the file matches patterns of both sonar.abap.filePatterns and sonar.cobol.filePatterns"); + recognizer.of(newInputFile("abc.txt")); recognizer.stop(); } - private Project newProject(String language) { - Project project = mock(Project.class); - when(project.getLanguageKey()).thenReturn(language); - return project; + private InputFile newInputFile(String path) throws IOException { + File basedir = temp.newFolder(); + return new InputFileBuilder(new File(basedir, path), Charsets.UTF_8, path).build(); } static class MockLanguage implements Language { diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java b/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java index dda95c0d131..0bc0fd04085 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java @@ -158,6 +158,10 @@ public interface CoreProperties { */ String PROJECT_DATE_PROPERTY = "sonar.projectDate"; + /** + * @deprecated since 4.2 project are now multi-language + */ + @Deprecated String PROJECT_LANGUAGE_PROPERTY = "sonar.language"; String DYNAMIC_ANALYSIS_PROPERTY = "sonar.dynamicAnalysis"; diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/resources/Languages.java b/sonar-plugin-api/src/main/java/org/sonar/api/resources/Languages.java index b062fe1fa8b..45736dd9b9c 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/resources/Languages.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/resources/Languages.java @@ -28,6 +28,7 @@ import org.sonar.api.ServerComponent; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -89,4 +90,12 @@ public class Languages implements BatchComponent, ServerComponent { map.put(language.getKey(), language); } + /** + * @since 4.2 + */ + public Language[] all() { + Collection<Language> languages = map.values(); + return languages.toArray(new Language[languages.size()]); + } + } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/resources/Project.java b/sonar-plugin-api/src/main/java/org/sonar/api/resources/Project.java index 610fe166957..eaa18c66e51 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/resources/Project.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/resources/Project.java @@ -229,8 +229,10 @@ public class Project extends Resource implements Component { } /** - * @return the project language + * @return the project language when there is only one language + * @deprecated since 4.2 */ + @Deprecated @Override public Language getLanguage() { return language; @@ -242,10 +244,12 @@ public class Project extends Resource implements Component { } /** - * @return the language key + * @return the language key or empty if no language is specified + * @deprecated since 4.2 */ + @Deprecated public String getLanguageKey() { - return configuration.getString("sonar.language", Java.KEY); + return configuration.getString(CoreProperties.PROJECT_LANGUAGE_PROPERTY, ""); } /** |