*/ | */ | ||||
package org.sonar.xoo; | package org.sonar.xoo; | ||||
import java.util.Arrays; | |||||
import org.sonar.api.config.Configuration; | |||||
import org.sonar.api.resources.Language; | import org.sonar.api.resources.Language; | ||||
public class Xoo2 implements Language { | public class Xoo2 implements Language { | ||||
public static final String KEY = "xoo2"; | public static final String KEY = "xoo2"; | ||||
public static final String NAME = "Xoo2"; | public static final String NAME = "Xoo2"; | ||||
public static final String FILE_SUFFIX = ".xoo2"; | |||||
public static final String FILE_SUFFIXES_KEY = "sonar.xoo2.file.suffixes"; | |||||
public static final String DEFAULT_FILE_SUFFIXES = ".xoo2"; | |||||
private static final String[] XOO_SUFFIXES = { | |||||
FILE_SUFFIX | |||||
}; | |||||
private final Configuration configuration; | |||||
public Xoo2(Configuration configuration) { | |||||
this.configuration = configuration; | |||||
} | |||||
@Override | @Override | ||||
public String getKey() { | public String getKey() { | ||||
@Override | @Override | ||||
public String[] getFileSuffixes() { | public String[] getFileSuffixes() { | ||||
return XOO_SUFFIXES; | |||||
return Arrays.stream(configuration.getStringArray(FILE_SUFFIXES_KEY)).filter(s -> s != null && !s.trim().isEmpty()).toArray(String[]::new); | |||||
} | } | ||||
@Override | @Override |
.onQualifiers(Qualifiers.PROJECT) | .onQualifiers(Qualifiers.PROJECT) | ||||
.multiValues(true) | .multiValues(true) | ||||
.build(), | .build(), | ||||
PropertyDefinition.builder(Xoo2.FILE_SUFFIXES_KEY) | |||||
.defaultValue(Xoo2.DEFAULT_FILE_SUFFIXES) | |||||
.name("File suffixes") | |||||
.description("Comma-separated list of suffixes for files to analyze. To not filter, leave the list empty.") | |||||
.subCategory("General") | |||||
.onQualifiers(Qualifiers.PROJECT) | |||||
.build(), | |||||
// Used by DuplicationsTest and IssueFilterOnCommonRulesTest. If not declared it is not returned by api/settings | // Used by DuplicationsTest and IssueFilterOnCommonRulesTest. If not declared it is not returned by api/settings | ||||
PropertyDefinition.builder("sonar.cpd.xoo.minimumTokens") | PropertyDefinition.builder("sonar.cpd.xoo.minimumTokens") | ||||
.onQualifiers(Qualifiers.PROJECT) | .onQualifiers(Qualifiers.PROJECT) |
local { | local { | ||||
enabled = !isCiServer | enabled = !isCiServer | ||||
} | } | ||||
} | |||||
} |
*/ | */ | ||||
package org.sonar.core.metric; | package org.sonar.core.metric; | ||||
import com.google.common.collect.ImmutableSet; | |||||
import java.util.Arrays; | |||||
import java.util.List; | import java.util.List; | ||||
import java.util.Objects; | import java.util.Objects; | ||||
import java.util.Set; | import java.util.Set; | ||||
import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||
import java.util.stream.Stream; | import java.util.stream.Stream; | ||||
import javax.annotation.concurrent.Immutable; | |||||
import org.sonar.api.ce.ComputeEngineSide; | import org.sonar.api.ce.ComputeEngineSide; | ||||
import org.sonar.api.measures.Metric; | import org.sonar.api.measures.Metric; | ||||
import org.sonar.api.measures.Metrics; | import org.sonar.api.measures.Metrics; | ||||
* <p/> | * <p/> | ||||
* Scanners should not send other metrics, and the Compute Engine should not allow other metrics. | * Scanners should not send other metrics, and the Compute Engine should not allow other metrics. | ||||
*/ | */ | ||||
@Immutable | |||||
@ComputeEngineSide | @ComputeEngineSide | ||||
@ScannerSide | @ScannerSide | ||||
public class ScannerMetrics { | public class ScannerMetrics { | ||||
private static final Set<Metric> ALLOWED_CORE_METRICS = ImmutableSet.of( | |||||
private static final Set<Metric> ALLOWED_CORE_METRICS = Set.of( | |||||
GENERATED_LINES, | GENERATED_LINES, | ||||
NCLOC, | NCLOC, | ||||
NCLOC_DATA, | NCLOC_DATA, | ||||
EXECUTABLE_LINES_DATA); | EXECUTABLE_LINES_DATA); | ||||
private final Set<Metric> metrics; | |||||
private Set<Metric> metrics; | |||||
@Autowired(required = false) | @Autowired(required = false) | ||||
public ScannerMetrics() { | public ScannerMetrics() { | ||||
} | } | ||||
@Autowired(required = false) | @Autowired(required = false) | ||||
public ScannerMetrics(Metrics[] metricsRepositories) { | |||||
this.metrics = Stream.concat(getPluginMetrics(metricsRepositories), ALLOWED_CORE_METRICS.stream()).collect(Collectors.toSet()); | |||||
public ScannerMetrics(List<Metrics> metricsRepositories) { | |||||
this.metrics = ALLOWED_CORE_METRICS; | |||||
addPluginMetrics(metricsRepositories); | |||||
} | } | ||||
/** | /** | ||||
return metrics; | return metrics; | ||||
} | } | ||||
private static Stream<Metric> getPluginMetrics(Metrics[] metricsRepositories) { | |||||
return Arrays.stream(metricsRepositories) | |||||
/** | |||||
* Adds the given metrics to the set of allowed metrics | |||||
*/ | |||||
public void addPluginMetrics(List<Metrics> metricsRepositories) { | |||||
this.metrics = Stream.concat(getPluginMetrics(metricsRepositories.stream()), this.metrics.stream()).collect(Collectors.toSet()); | |||||
} | |||||
private static Stream<Metric> getPluginMetrics(Stream<Metrics> metricsStream) { | |||||
return metricsStream | |||||
.map(Metrics::getMetrics) | .map(Metrics::getMetrics) | ||||
.filter(Objects::nonNull) | .filter(Objects::nonNull) | ||||
.flatMap(List::stream); | .flatMap(List::stream); |
*/ | */ | ||||
package org.sonar.core.platform; | package org.sonar.core.platform; | ||||
import java.util.List; | |||||
import java.util.Set; | import java.util.Set; | ||||
import javax.annotation.CheckForNull; | import javax.annotation.CheckForNull; | ||||
import javax.annotation.Nullable; | import javax.annotation.Nullable; | ||||
Set<Class<?>> getWebApiV2ConfigurationClasses(); | Set<Class<?>> getWebApiV2ConfigurationClasses(); | ||||
<T> T getParentComponentByType(Class<T> type); | |||||
<T> List<T> getParentComponentsByType(Class<T> type); | |||||
@CheckForNull | @CheckForNull | ||||
ExtensionContainer getParent(); | ExtensionContainer getParent(); | ||||
} | } |
return webConfigurationClasses; | return webConfigurationClasses; | ||||
} | } | ||||
@Override | |||||
public <T> T getParentComponentByType(Class<T> type) { | |||||
throw new UnsupportedOperationException(); | |||||
} | |||||
@Override | |||||
public <T> List<T> getParentComponentsByType(Class<T> type) { | |||||
throw new UnsupportedOperationException(); | |||||
} | |||||
@Override | @Override | ||||
public ExtensionContainer getParent() { | public ExtensionContainer getParent() { | ||||
throw new UnsupportedOperationException(); | throw new UnsupportedOperationException(); |
this(parent, parent.propertyDefinitions, emptyList(), new LazyUnlessStartableStrategy()); | this(parent, parent.propertyDefinitions, emptyList(), new LazyUnlessStartableStrategy()); | ||||
} | } | ||||
protected SpringComponentContainer(SpringComponentContainer parent, List<?> externalExtensions) { | |||||
this(parent, parent.propertyDefinitions, externalExtensions, new LazyUnlessStartableStrategy()); | |||||
} | |||||
protected SpringComponentContainer(SpringComponentContainer parent, SpringInitStrategy initStrategy) { | protected SpringComponentContainer(SpringComponentContainer parent, SpringInitStrategy initStrategy) { | ||||
this(parent, parent.propertyDefinitions, emptyList(), initStrategy); | this(parent, parent.propertyDefinitions, emptyList(), initStrategy); | ||||
} | } | ||||
add(propertyDefs); | add(propertyDefs); | ||||
} | } | ||||
//TODO: To be removed, added for moving on with the non matching LanguagesRepository beans | |||||
public void addIfMissing(Object object, Class<?> objectType) { | |||||
try { | |||||
getParentComponentByType(objectType); | |||||
} catch (IllegalStateException e) { | |||||
add(object); | |||||
} | |||||
} | |||||
/** | /** | ||||
* Beans need to have a unique name, otherwise they'll override each other. | * Beans need to have a unique name, otherwise they'll override each other. | ||||
* The strategy is: | * The strategy is: | ||||
return Set.copyOf(webConfigurationClasses); | return Set.copyOf(webConfigurationClasses); | ||||
} | } | ||||
@Override | |||||
public <T> T getParentComponentByType(Class<T> type) { | |||||
if (parent == null) { | |||||
throw new IllegalStateException("No parent container"); | |||||
} else { | |||||
return parent.getComponentByType(type); | |||||
} | |||||
} | |||||
@Override | |||||
public <T> List<T> getParentComponentsByType(Class<T> type) { | |||||
if (parent == null) { | |||||
throw new IllegalStateException("No parent container"); | |||||
} else { | |||||
return parent.getComponentsByType(type); | |||||
} | |||||
} | |||||
private <T> void registerInstance(T instance) { | private <T> void registerInstance(T instance) { | ||||
Supplier<T> supplier = () -> instance; | Supplier<T> supplier = () -> instance; | ||||
Class<T> clazz = (Class<T>) instance.getClass(); | Class<T> clazz = (Class<T>) instance.getClass(); |
assertThat(metrics).isEqualTo(okMetrics.getMetrics()); | assertThat(metrics).isEqualTo(okMetrics.getMetrics()); | ||||
} | } | ||||
@Test | |||||
public void should_add_new_plugin_metrics() { | |||||
Metrics fakeMetrics = new FakeMetrics(); | |||||
Metrics fakeMetrics2 = new FakeMetrics2(); | |||||
ScannerMetrics underTest = new ScannerMetrics(List.of(fakeMetrics)); | |||||
assertThat(underTest.getMetrics()).hasSize(24); | |||||
assertThat(underTest.getMetrics()).containsAll(fakeMetrics.getMetrics()); | |||||
underTest.addPluginMetrics(List.of( fakeMetrics, fakeMetrics2 )); | |||||
assertThat(underTest.getMetrics()).hasSize(25); | |||||
assertThat(underTest.getMetrics()).containsAll(fakeMetrics.getMetrics()); | |||||
assertThat(underTest.getMetrics()).containsAll(fakeMetrics2.getMetrics()); | |||||
} | |||||
private static List<Metric> metrics(Metrics... metrics) { | private static List<Metric> metrics(Metrics... metrics) { | ||||
return new ArrayList<>(new ScannerMetrics(metrics).getMetrics()); | |||||
return new ArrayList<>(new ScannerMetrics(Arrays.asList(metrics)).getMetrics()); | |||||
} | } | ||||
private static class FakeMetrics implements Metrics { | private static class FakeMetrics implements Metrics { | ||||
new Metric.Builder("key2", "name2", Metric.ValueType.FLOAT).create()); | new Metric.Builder("key2", "name2", Metric.ValueType.FLOAT).create()); | ||||
} | } | ||||
} | } | ||||
private static class FakeMetrics2 implements Metrics { | |||||
@Override | |||||
public List<Metric> getMetrics() { | |||||
return Arrays.asList( | |||||
new Metric.Builder("key3", "name1", Metric.ValueType.INT).create()); | |||||
} | |||||
} | |||||
} | } |
private final FakeActiveRulesLoader activeRules = new FakeActiveRulesLoader(); | private final FakeActiveRulesLoader activeRules = new FakeActiveRulesLoader(); | ||||
private final FakeSonarRuntime sonarRuntime = new FakeSonarRuntime(); | private final FakeSonarRuntime sonarRuntime = new FakeSonarRuntime(); | ||||
private final CeTaskReportDataHolder reportMetadataHolder = new CeTaskReportDataHolderExt(); | private final CeTaskReportDataHolder reportMetadataHolder = new CeTaskReportDataHolderExt(); | ||||
private final FakeLanguagesRepository languagesRepository = new FakeLanguagesRepository(); | |||||
private LogOutput logOutput = null; | private LogOutput logOutput = null; | ||||
private static void createWorkingDirs() throws IOException { | private static void createWorkingDirs() throws IOException { | ||||
return builder; | return builder; | ||||
} | } | ||||
public void addLanguage(String key, String name, String... suffixes) { | |||||
languagesRepository.addLanguage(key, name, suffixes, new String[0]); | |||||
} | |||||
public void addLanguage(String key, String name, boolean publishAllFiles, String... suffixes) { | |||||
languagesRepository.addLanguage(key, name, suffixes, new String[0], publishAllFiles); | |||||
} | |||||
public static class AnalysisBuilder { | public static class AnalysisBuilder { | ||||
private final Map<String, String> taskProperties = new HashMap<>(); | private final Map<String, String> taskProperties = new HashMap<>(); | ||||
private final ScannerMediumTester tester; | private final ScannerMediumTester tester; | ||||
tester.analysisCacheLoader, | tester.analysisCacheLoader, | ||||
tester.sonarRuntime, | tester.sonarRuntime, | ||||
tester.reportMetadataHolder, | tester.reportMetadataHolder, | ||||
result); | |||||
result, | |||||
tester.languagesRepository); | |||||
if (tester.logOutput != null) { | if (tester.logOutput != null) { | ||||
builder.setLogOutput(tester.logOutput); | builder.setLogOutput(tester.logOutput); | ||||
} else { | } else { |
File srcDir = new File(baseDir, "src"); | File srcDir = new File(baseDir, "src"); | ||||
srcDir.mkdir(); | srcDir.mkdir(); | ||||
tester.addLanguage("xoo3", "xoo3",false, ".xoo3"); | |||||
writeFile(srcDir, "sample.xoo3", "Sample xoo\ncontent"); | writeFile(srcDir, "sample.xoo3", "Sample xoo\ncontent"); | ||||
writeFile(srcDir, "sample2.xoo3", "Sample xoo 2\ncontent"); | writeFile(srcDir, "sample2.xoo3", "Sample xoo 2\ncontent"); | ||||
assertThat(result.inputFiles()).hasSize(2); | assertThat(result.inputFiles()).hasSize(2); | ||||
tester.addLanguage("xoo2", "xoo2", ".xoo"); | |||||
AnalysisBuilder analysisBuilder = tester.newAnalysis() | AnalysisBuilder analysisBuilder = tester.newAnalysis() | ||||
.properties(builder | .properties(builder | ||||
.put("sonar.lang.patterns.xoo2", "**/*.xoo") | .put("sonar.lang.patterns.xoo2", "**/*.xoo") |
import org.junit.Test; | import org.junit.Test; | ||||
import org.junit.rules.TemporaryFolder; | import org.junit.rules.TemporaryFolder; | ||||
import org.sonar.scanner.mediumtest.ScannerMediumTester; | import org.sonar.scanner.mediumtest.ScannerMediumTester; | ||||
import org.springframework.beans.factory.BeanCreationException; | |||||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||||
assertThatThrownBy(() -> tester | assertThatThrownBy(() -> tester | ||||
.newAnalysis(new File(projectDir, "sonar-project.properties")) | .newAnalysis(new File(projectDir, "sonar-project.properties")) | ||||
.execute()) | .execute()) | ||||
.isInstanceOf(BeanCreationException.class) | |||||
.hasRootCauseMessage("No language plugins are installed."); | |||||
.isInstanceOf(IllegalStateException.class) | |||||
.hasMessage("No language plugins are installed."); | |||||
} | } | ||||
private File copyProject(String path) throws Exception { | private File copyProject(String path) throws Exception { |
/* | |||||
* SonarQube | |||||
* Copyright (C) 2009-2024 SonarSource SA | |||||
* mailto:info AT sonarsource DOT com | |||||
* | |||||
* This program 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. | |||||
* | |||||
* This program 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.scanner.mediumtest.properties; | |||||
import com.google.common.collect.ImmutableMap; | |||||
import java.io.File; | |||||
import java.io.IOException; | |||||
import java.nio.charset.StandardCharsets; | |||||
import java.util.HashMap; | |||||
import java.util.Map; | |||||
import org.apache.commons.io.FileUtils; | |||||
import org.junit.Rule; | |||||
import org.junit.Test; | |||||
import org.junit.rules.TemporaryFolder; | |||||
import org.sonar.api.SonarEdition; | |||||
import org.sonar.api.testfixtures.log.LogTester; | |||||
import org.sonar.scanner.mediumtest.ScannerMediumTester; | |||||
import org.sonar.scanner.protocol.output.FileStructure; | |||||
import org.sonar.scanner.protocol.output.ScannerReportReader; | |||||
import org.sonar.xoo.XooPlugin; | |||||
import org.sonar.xoo.rule.XooRulesDefinition; | |||||
import static org.assertj.core.api.Assertions.assertThat; | |||||
public class PropertiesMediumIT { | |||||
@Rule | |||||
public TemporaryFolder temp = new TemporaryFolder(); | |||||
@Rule | |||||
public LogTester logTester = new LogTester(); | |||||
@Rule | |||||
public ScannerMediumTester tester = new ScannerMediumTester() | |||||
.setEdition(SonarEdition.ENTERPRISE) | |||||
.registerPlugin("xoo", new XooPlugin()) | |||||
.addDefaultQProfile("xoo", "Sonar Way") | |||||
.addRules(new XooRulesDefinition()) | |||||
// active a rule just to be sure that xoo files are published | |||||
.addActiveRule("xoo", "xoo:OneIssuePerFile", null, "One Issue Per File", null, null, null); | |||||
@Test | |||||
public void testProperties() throws IOException { | |||||
File baseDir = prepareProject(); | |||||
tester.newAnalysis() | |||||
.properties(ImmutableMap.<String, String>builder() | |||||
.put("sonar.projectBaseDir", baseDir.getAbsolutePath()) | |||||
.put("sonar.projectKey", "com.foo.project") | |||||
.put("sonar.sources", "src") | |||||
.put("sonar.analysis.property", "value") | |||||
.build()) | |||||
.execute(); | |||||
var properties = getProperties(baseDir); | |||||
//We focus on the specific property that we would like to get added to the report | |||||
assertThat(properties).containsEntry("sonar.analysis.property", "value"); | |||||
} | |||||
private Map<String, String> getProperties(File baseDir) { | |||||
File reportDir = new File(baseDir, ".sonar/scanner-report"); | |||||
FileStructure fileStructure = new FileStructure(reportDir); | |||||
ScannerReportReader reader = new ScannerReportReader(fileStructure); | |||||
Map<String, String> properties = new HashMap<>(); | |||||
try (var iterator = reader.readContextProperties()) { | |||||
iterator.forEachRemaining(p -> properties.put(p.getKey(), p.getValue())); | |||||
} | |||||
return properties; | |||||
} | |||||
private File prepareProject() throws IOException { | |||||
File baseDir = temp.getRoot(); | |||||
File srcDir = new File(baseDir, "src"); | |||||
srcDir.mkdir(); | |||||
File xooFile1 = new File(srcDir, "sample.xoo"); | |||||
FileUtils.write(xooFile1, "Sample xoo\ncontent\n3\n4\n5", StandardCharsets.UTF_8); | |||||
return baseDir; | |||||
} | |||||
} |
*/ | */ | ||||
package org.sonar.scanner.bootstrap; | package org.sonar.scanner.bootstrap; | ||||
import java.util.Collection; | |||||
import javax.annotation.Nullable; | import javax.annotation.Nullable; | ||||
import org.sonar.api.Plugin; | import org.sonar.api.Plugin; | ||||
import org.sonar.api.SonarRuntime; | import org.sonar.api.SonarRuntime; | ||||
} | } | ||||
// plugin extensions | // plugin extensions | ||||
for (PluginInfo pluginInfo : pluginRepository.getPluginInfos()) { | |||||
installExtensionsForPlugins(container, matcher, pluginRepository.getPluginInfos()); | |||||
return this; | |||||
} | |||||
public void installExtensionsForPlugins(ExtensionContainer container, ExtensionMatcher matcher, Collection<PluginInfo> pluginInfos) { | |||||
for (PluginInfo pluginInfo : pluginInfos) { | |||||
Plugin plugin = pluginRepository.getPluginInstance(pluginInfo.getKey()); | Plugin plugin = pluginRepository.getPluginInstance(pluginInfo.getKey()); | ||||
Plugin.Context context = new PluginContextImpl.Builder() | Plugin.Context context = new PluginContextImpl.Builder() | ||||
.setSonarRuntime(sonarRuntime) | .setSonarRuntime(sonarRuntime) | ||||
doInstall(container, matcher, pluginInfo, extension); | doInstall(container, matcher, pluginInfo, extension); | ||||
} | } | ||||
} | } | ||||
return this; | |||||
} | } | ||||
private static void doInstall(ExtensionContainer container, ExtensionMatcher matcher, @Nullable PluginInfo pluginInfo, Object extension) { | private static void doInstall(ExtensionContainer container, ExtensionMatcher matcher, @Nullable PluginInfo pluginInfo, Object extension) { |
import java.util.List; | import java.util.List; | ||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.Set; | |||||
public interface PluginInstaller { | public interface PluginInstaller { | ||||
/** | /** | ||||
* Gets the list of plugins installed on server and downloads them if not | |||||
* Loads/downloads all plugins that are installed on the server. | |||||
* @return information about all installed plugins, grouped by key | |||||
*/ | |||||
Map<String, ScannerPlugin> installAllPlugins(); | |||||
/** | |||||
* Gets the list of plugins that are not required for any specific languages and downloads them if not | |||||
* already in local cache. | * already in local cache. | ||||
* @return information about all installed plugins, grouped by key | * @return information about all installed plugins, grouped by key | ||||
*/ | */ | ||||
Map<String, ScannerPlugin> installRemotes(); | |||||
Map<String, ScannerPlugin> installRequiredPlugins(); | |||||
/** | |||||
* Loads/downloads plugins that are required for the given languageKeys. | |||||
* @return information about any plugins installed by this call, grouped by key | |||||
*/ | |||||
Map<String, ScannerPlugin> installPluginsForLanguages(Set<String> languageKeys); | |||||
/** | /** | ||||
* Used only by medium tests. | * Used only by medium tests. |
import com.google.gson.Gson; | import com.google.gson.Gson; | ||||
import java.io.File; | import java.io.File; | ||||
import java.io.Reader; | import java.io.Reader; | ||||
import java.util.ArrayList; | |||||
import java.util.Collections; | import java.util.Collections; | ||||
import java.util.HashMap; | import java.util.HashMap; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.Optional; | import java.util.Optional; | ||||
import java.util.Set; | |||||
import java.util.function.Predicate; | |||||
import javax.annotation.Nullable; | import javax.annotation.Nullable; | ||||
import org.sonar.api.utils.log.Logger; | import org.sonar.api.utils.log.Logger; | ||||
import org.sonar.api.utils.log.Loggers; | import org.sonar.api.utils.log.Loggers; | ||||
private final PluginFiles pluginFiles; | private final PluginFiles pluginFiles; | ||||
private final DefaultScannerWsClient wsClient; | private final DefaultScannerWsClient wsClient; | ||||
private List<InstalledPlugin> availablePlugins; | |||||
public ScannerPluginInstaller(PluginFiles pluginFiles, DefaultScannerWsClient wsClient) { | public ScannerPluginInstaller(PluginFiles pluginFiles, DefaultScannerWsClient wsClient) { | ||||
this.pluginFiles = pluginFiles; | this.pluginFiles = pluginFiles; | ||||
this.wsClient = wsClient; | this.wsClient = wsClient; | ||||
} | } | ||||
@Override | @Override | ||||
public Map<String, ScannerPlugin> installRemotes() { | |||||
public Map<String, ScannerPlugin> installAllPlugins() { | |||||
LOG.info("Loading all plugins"); | |||||
return installPlugins(p -> true).installedPluginsByKey; | |||||
} | |||||
@Override | |||||
public Map<String, ScannerPlugin> installRequiredPlugins() { | |||||
LOG.info("Loading required plugins"); | |||||
InstallResult result = installPlugins(p -> p.getRequiredForLanguages() == null || p.getRequiredForLanguages().isEmpty()); | |||||
LOG.debug("Plugins not loaded because they are optional: {}", result.skippedPlugins); | |||||
return result.installedPluginsByKey; | |||||
} | |||||
@Override | |||||
public Map<String, ScannerPlugin> installPluginsForLanguages(Set<String> languageKeys) { | |||||
LOG.info("Loading plugins for detected languages"); | |||||
LOG.debug("Detected languages: {}", languageKeys); | |||||
InstallResult result = installPlugins( | |||||
p -> p.getRequiredForLanguages() != null && !Collections.disjoint(p.getRequiredForLanguages(), languageKeys) | |||||
); | |||||
List<InstalledPlugin> skippedLanguagePlugins = result.skippedPlugins.stream() | |||||
.filter(p -> p.getRequiredForLanguages() != null && !p.getRequiredForLanguages().isEmpty()).toList(); | |||||
LOG.debug("Optional language-specific plugins not loaded: {}", skippedLanguagePlugins); | |||||
return result.installedPluginsByKey; | |||||
} | |||||
private InstallResult installPlugins(Predicate<InstalledPlugin> pluginFilter) { | |||||
if (this.availablePlugins == null) { | |||||
this.availablePlugins = listInstalledPlugins(); | |||||
} | |||||
Profiler profiler = Profiler.create(LOG).startInfo("Load/download plugins"); | Profiler profiler = Profiler.create(LOG).startInfo("Load/download plugins"); | ||||
try { | try { | ||||
Map<String, ScannerPlugin> result = new HashMap<>(); | |||||
Loaded loaded = loadPlugins(result); | |||||
InstallResult result = new InstallResult(); | |||||
Loaded loaded = loadPlugins(result, pluginFilter); | |||||
if (!loaded.ok) { | if (!loaded.ok) { | ||||
// retry once, a plugin may have been uninstalled during downloads | // retry once, a plugin may have been uninstalled during downloads | ||||
result.clear(); | |||||
loaded = loadPlugins(result); | |||||
this.availablePlugins = listInstalledPlugins(); | |||||
result.installedPluginsByKey.clear(); | |||||
loaded = loadPlugins(result, pluginFilter); | |||||
if (!loaded.ok) { | if (!loaded.ok) { | ||||
throw new IllegalStateException(format("Fail to download plugin [%s]. Not found.", loaded.notFoundPlugin)); | throw new IllegalStateException(format("Fail to download plugin [%s]. Not found.", loaded.notFoundPlugin)); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
private Loaded loadPlugins(Map<String, ScannerPlugin> result) { | |||||
for (InstalledPlugin plugin : listInstalledPlugins()) { | |||||
private Loaded loadPlugins(InstallResult result, Predicate<InstalledPlugin> pluginFilter) { | |||||
List<InstalledPlugin> pluginsToInstall = availablePlugins.stream() | |||||
.filter(pluginFilter).toList(); | |||||
for (InstalledPlugin plugin : pluginsToInstall) { | |||||
Optional<File> jarFile = pluginFiles.get(plugin); | Optional<File> jarFile = pluginFiles.get(plugin); | ||||
if (jarFile.isEmpty()) { | if (jarFile.isEmpty()) { | ||||
return new Loaded(false, plugin.key); | return new Loaded(false, plugin.key); | ||||
} | } | ||||
PluginInfo info = PluginInfo.create(jarFile.get()); | PluginInfo info = PluginInfo.create(jarFile.get()); | ||||
result.put(info.getKey(), new ScannerPlugin(plugin.key, plugin.updatedAt, PluginType.valueOf(plugin.type), info)); | |||||
result.installedPluginsByKey.put(info.getKey(), new ScannerPlugin(plugin.key, plugin.updatedAt, PluginType.valueOf(plugin.type), info)); | |||||
} | } | ||||
result.skippedPlugins = availablePlugins.stream() | |||||
.filter(Predicate.not(pluginFilter)).toList(); | |||||
return new Loaded(true, null); | return new Loaded(true, null); | ||||
} | } | ||||
/** | /** | ||||
* Gets information about the plugins installed on server (filename, checksum) | * Gets information about the plugins installed on server (filename, checksum) | ||||
*/ | */ | ||||
private InstalledPlugin[] listInstalledPlugins() { | |||||
private List<InstalledPlugin> listInstalledPlugins() { | |||||
Profiler profiler = Profiler.create(LOG).startInfo("Load plugins index"); | Profiler profiler = Profiler.create(LOG).startInfo("Load plugins index"); | ||||
GetRequest getRequest = new GetRequest(PLUGINS_WS_URL); | GetRequest getRequest = new GetRequest(PLUGINS_WS_URL); | ||||
InstalledPlugins installedPlugins; | InstalledPlugins installedPlugins; | ||||
return installedPlugins.plugins; | return installedPlugins.plugins; | ||||
} | } | ||||
private static class InstallResult { | |||||
Map<String, ScannerPlugin> installedPluginsByKey = new HashMap<>(); | |||||
List<InstalledPlugin> skippedPlugins = new ArrayList<>(); | |||||
} | |||||
private static class InstalledPlugins { | private static class InstalledPlugins { | ||||
InstalledPlugin[] plugins; | |||||
List<InstalledPlugin> plugins; | |||||
public InstalledPlugins() { | public InstalledPlugins() { | ||||
// http://stackoverflow.com/a/18645370/229031 | // http://stackoverflow.com/a/18645370/229031 | ||||
String hash; | String hash; | ||||
long updatedAt; | long updatedAt; | ||||
String type; | String type; | ||||
private Set<String> requiredForLanguages; | |||||
public InstalledPlugin() { | public InstalledPlugin() { | ||||
// http://stackoverflow.com/a/18645370/229031 | // http://stackoverflow.com/a/18645370/229031 | ||||
} | } | ||||
public Set<String> getRequiredForLanguages() { | |||||
return requiredForLanguages; | |||||
} | |||||
@Override | |||||
public String toString() { | |||||
return key; | |||||
} | |||||
} | } | ||||
private static class Loaded { | private static class Loaded { |
package org.sonar.scanner.bootstrap; | package org.sonar.scanner.bootstrap; | ||||
import java.util.Collection; | import java.util.Collection; | ||||
import java.util.Collections; | |||||
import java.util.HashMap; | import java.util.HashMap; | ||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.stream.Collectors; | |||||
import java.util.Set; | |||||
import javax.annotation.CheckForNull; | import javax.annotation.CheckForNull; | ||||
import org.slf4j.Logger; | import org.slf4j.Logger; | ||||
import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||
import org.sonar.api.Plugin; | import org.sonar.api.Plugin; | ||||
import org.sonar.api.Startable; | import org.sonar.api.Startable; | ||||
import org.sonar.api.config.Configuration; | |||||
import org.sonar.core.platform.ExplodedPlugin; | import org.sonar.core.platform.ExplodedPlugin; | ||||
import org.sonar.core.platform.PluginClassLoader; | import org.sonar.core.platform.PluginClassLoader; | ||||
import org.sonar.core.platform.PluginInfo; | import org.sonar.core.platform.PluginInfo; | ||||
import org.sonar.core.platform.PluginRepository; | import org.sonar.core.platform.PluginRepository; | ||||
import org.sonar.core.plugin.PluginType; | import org.sonar.core.plugin.PluginType; | ||||
import static java.util.stream.Collectors.toMap; | |||||
import static org.sonar.api.utils.Preconditions.checkState; | import static org.sonar.api.utils.Preconditions.checkState; | ||||
/** | /** | ||||
private final PluginJarExploder pluginJarExploder; | private final PluginJarExploder pluginJarExploder; | ||||
private final PluginClassLoader loader; | private final PluginClassLoader loader; | ||||
private final Configuration properties; | |||||
private Map<String, Plugin> pluginInstancesByKeys; | private Map<String, Plugin> pluginInstancesByKeys; | ||||
private Map<String, ScannerPlugin> pluginsByKeys; | private Map<String, ScannerPlugin> pluginsByKeys; | ||||
private Map<ClassLoader, String> keysByClassLoader; | private Map<ClassLoader, String> keysByClassLoader; | ||||
private boolean shouldLoadAllPluginsOnStart; | |||||
public ScannerPluginRepository(PluginInstaller installer, PluginJarExploder pluginJarExploder, PluginClassLoader loader) { | |||||
public ScannerPluginRepository(PluginInstaller installer, PluginJarExploder pluginJarExploder, PluginClassLoader loader, Configuration properties) { | |||||
this.installer = installer; | this.installer = installer; | ||||
this.pluginJarExploder = pluginJarExploder; | this.pluginJarExploder = pluginJarExploder; | ||||
this.loader = loader; | this.loader = loader; | ||||
this.properties = properties; | |||||
} | } | ||||
@Override | @Override | ||||
public void start() { | public void start() { | ||||
pluginsByKeys = new HashMap<>(installer.installRemotes()); | |||||
Map<String, ExplodedPlugin> explodedPLuginsByKey = pluginsByKeys.entrySet().stream() | |||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> pluginJarExploder.explode(e.getValue().getInfo()))); | |||||
pluginInstancesByKeys = new HashMap<>(loader.load(explodedPLuginsByKey)); | |||||
shouldLoadAllPluginsOnStart = properties.getBoolean("sonar.plugins.loadAll").orElse(false); | |||||
if (shouldLoadAllPluginsOnStart) { | |||||
LOG.warn("sonar.plugins.loadAll is true, so ALL available plugins will be downloaded"); | |||||
pluginsByKeys = new HashMap<>(installer.installAllPlugins()); | |||||
} else { | |||||
pluginsByKeys = new HashMap<>(installer.installRequiredPlugins()); | |||||
} | |||||
Map<String, ExplodedPlugin> explodedPluginsByKey = pluginsByKeys.entrySet().stream() | |||||
.collect(toMap(Map.Entry::getKey, e -> pluginJarExploder.explode(e.getValue().getInfo()))); | |||||
pluginInstancesByKeys = new HashMap<>(loader.load(explodedPluginsByKey)); | |||||
// this part is only used by medium tests | // this part is only used by medium tests | ||||
for (Object[] localPlugin : installer.installLocals()) { | for (Object[] localPlugin : installer.installLocals()) { | ||||
keysByClassLoader.put(e.getValue().getClass().getClassLoader(), e.getKey()); | keysByClassLoader.put(e.getValue().getClass().getClassLoader(), e.getKey()); | ||||
} | } | ||||
logPlugins(); | |||||
logPlugins(pluginsByKeys.values()); | |||||
} | |||||
public Collection<PluginInfo> installPluginsForLanguages(Set<String> languageKeys) { | |||||
if (shouldLoadAllPluginsOnStart) { | |||||
return Collections.emptySet(); | |||||
} | |||||
var languagePluginsByKeys = new HashMap<>(installer.installPluginsForLanguages(languageKeys)); | |||||
pluginsByKeys.putAll(languagePluginsByKeys); | |||||
Map<String, ExplodedPlugin> explodedPluginsByKey = languagePluginsByKeys.entrySet().stream() | |||||
.collect(toMap(Map.Entry::getKey, e -> pluginJarExploder.explode(e.getValue().getInfo()))); | |||||
pluginInstancesByKeys.putAll(new HashMap<>(loader.load(explodedPluginsByKey))); | |||||
keysByClassLoader = new HashMap<>(); | |||||
for (Map.Entry<String, Plugin> e : pluginInstancesByKeys.entrySet()) { | |||||
keysByClassLoader.put(e.getValue().getClass().getClassLoader(), e.getKey()); | |||||
} | |||||
logPlugins(languagePluginsByKeys.values()); | |||||
return languagePluginsByKeys.values().stream().map(ScannerPlugin::getInfo).toList(); | |||||
} | } | ||||
@CheckForNull | @CheckForNull | ||||
return keysByClassLoader.get(cl); | return keysByClassLoader.get(cl); | ||||
} | } | ||||
private void logPlugins() { | |||||
if (pluginsByKeys.isEmpty()) { | |||||
private static void logPlugins(Collection<ScannerPlugin> plugins) { | |||||
if (plugins.isEmpty()) { | |||||
LOG.debug("No plugins loaded"); | LOG.debug("No plugins loaded"); | ||||
} else { | } else { | ||||
LOG.debug("Plugins:"); | |||||
for (ScannerPlugin p : pluginsByKeys.values()) { | |||||
LOG.debug("Plugins loaded:"); | |||||
for (ScannerPlugin p : plugins) { | |||||
LOG.debug(" * {} {} ({})", p.getName(), p.getVersion(), p.getKey()); | LOG.debug(" * {} {} ({})", p.getName(), p.getVersion(), p.getKey()); | ||||
} | } | ||||
} | } |
import org.sonar.scanner.repository.DefaultNewCodePeriodLoader; | import org.sonar.scanner.repository.DefaultNewCodePeriodLoader; | ||||
import org.sonar.scanner.repository.MetricsRepositoryProvider; | import org.sonar.scanner.repository.MetricsRepositoryProvider; | ||||
import org.sonar.scanner.repository.settings.DefaultGlobalSettingsLoader; | import org.sonar.scanner.repository.settings.DefaultGlobalSettingsLoader; | ||||
import org.sonar.scanner.scan.SpringProjectScanContainer; | |||||
@Priority(3) | |||||
@Priority(4) | |||||
public class SpringGlobalContainer extends SpringComponentContainer { | public class SpringGlobalContainer extends SpringComponentContainer { | ||||
private static final Logger LOG = LoggerFactory.getLogger(SpringGlobalContainer.class); | private static final Logger LOG = LoggerFactory.getLogger(SpringGlobalContainer.class); | ||||
private final Map<String, String> scannerProperties; | private final Map<String, String> scannerProperties; | ||||
@Override | @Override | ||||
protected void doAfterStart() { | protected void doAfterStart() { | ||||
installPlugins(); | |||||
installRequiredPlugins(); | |||||
loadCoreExtensions(); | loadCoreExtensions(); | ||||
long startTime = System.currentTimeMillis(); | long startTime = System.currentTimeMillis(); | ||||
throw MessageException.of("The preview mode, along with the 'sonar.analysis.mode' parameter, is no more supported. You should stop using this parameter."); | throw MessageException.of("The preview mode, along with the 'sonar.analysis.mode' parameter, is no more supported. You should stop using this parameter."); | ||||
} | } | ||||
getComponentByType(RuntimeJavaVersion.class).checkJavaVersion(); | getComponentByType(RuntimeJavaVersion.class).checkJavaVersion(); | ||||
new SpringProjectScanContainer(this).execute(); | |||||
new SpringScannerContainer(this).execute(); | |||||
LOG.info("Analysis total time: {}", formatTime(System.currentTimeMillis() - startTime)); | LOG.info("Analysis total time: {}", formatTime(System.currentTimeMillis() - startTime)); | ||||
} | } | ||||
private void installPlugins() { | |||||
private void installRequiredPlugins() { | |||||
PluginRepository pluginRepository = getComponentByType(PluginRepository.class); | PluginRepository pluginRepository = getComponentByType(PluginRepository.class); | ||||
for (PluginInfo pluginInfo : pluginRepository.getPluginInfos()) { | for (PluginInfo pluginInfo : pluginRepository.getPluginInfos()) { | ||||
Plugin instance = pluginRepository.getPluginInstance(pluginInfo.getKey()); | Plugin instance = pluginRepository.getPluginInstance(pluginInfo.getKey()); |
/* | |||||
* SonarQube | |||||
* Copyright (C) 2009-2024 SonarSource SA | |||||
* mailto:info AT sonarsource DOT com | |||||
* | |||||
* This program 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. | |||||
* | |||||
* This program 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.scanner.bootstrap; | |||||
import javax.annotation.Nullable; | |||||
import javax.annotation.Priority; | |||||
import org.slf4j.Logger; | |||||
import org.slf4j.LoggerFactory; | |||||
import org.sonar.api.batch.fs.internal.FileMetadata; | |||||
import org.sonar.api.batch.fs.internal.SensorStrategy; | |||||
import org.sonar.api.batch.rule.CheckFactory; | |||||
import org.sonar.api.batch.sensor.issue.internal.DefaultNoSonarFilter; | |||||
import org.sonar.api.scan.filesystem.PathResolver; | |||||
import org.sonar.api.utils.MessageException; | |||||
import org.sonar.core.extension.CoreExtensionsInstaller; | |||||
import org.sonar.core.metric.ScannerMetrics; | |||||
import org.sonar.core.platform.SpringComponentContainer; | |||||
import org.sonar.scanner.DefaultFileLinesContextFactory; | |||||
import org.sonar.scanner.ProjectInfo; | |||||
import org.sonar.scanner.analysis.AnalysisTempFolderProvider; | |||||
import org.sonar.scanner.cache.AnalysisCacheEnabled; | |||||
import org.sonar.scanner.cache.AnalysisCacheMemoryStorage; | |||||
import org.sonar.scanner.cache.AnalysisCacheProvider; | |||||
import org.sonar.scanner.cache.DefaultAnalysisCacheLoader; | |||||
import org.sonar.scanner.ci.CiConfigurationProvider; | |||||
import org.sonar.scanner.ci.vendors.AppVeyor; | |||||
import org.sonar.scanner.ci.vendors.AwsCodeBuild; | |||||
import org.sonar.scanner.ci.vendors.AzureDevops; | |||||
import org.sonar.scanner.ci.vendors.Bamboo; | |||||
import org.sonar.scanner.ci.vendors.BitbucketPipelines; | |||||
import org.sonar.scanner.ci.vendors.Bitrise; | |||||
import org.sonar.scanner.ci.vendors.Buildkite; | |||||
import org.sonar.scanner.ci.vendors.CircleCi; | |||||
import org.sonar.scanner.ci.vendors.CirrusCi; | |||||
import org.sonar.scanner.ci.vendors.CodeMagic; | |||||
import org.sonar.scanner.ci.vendors.DroneCi; | |||||
import org.sonar.scanner.ci.vendors.GithubActions; | |||||
import org.sonar.scanner.ci.vendors.GitlabCi; | |||||
import org.sonar.scanner.ci.vendors.Jenkins; | |||||
import org.sonar.scanner.ci.vendors.SemaphoreCi; | |||||
import org.sonar.scanner.ci.vendors.TravisCi; | |||||
import org.sonar.scanner.cpd.CpdExecutor; | |||||
import org.sonar.scanner.cpd.CpdSettings; | |||||
import org.sonar.scanner.cpd.index.SonarCpdBlockIndex; | |||||
import org.sonar.scanner.issue.IssueFilters; | |||||
import org.sonar.scanner.issue.IssuePublisher; | |||||
import org.sonar.scanner.issue.ignore.EnforceIssuesFilter; | |||||
import org.sonar.scanner.issue.ignore.IgnoreIssuesFilter; | |||||
import org.sonar.scanner.issue.ignore.pattern.IssueExclusionPatternInitializer; | |||||
import org.sonar.scanner.issue.ignore.pattern.IssueInclusionPatternInitializer; | |||||
import org.sonar.scanner.issue.ignore.scanner.IssueExclusionsLoader; | |||||
import org.sonar.scanner.report.ActiveRulesPublisher; | |||||
import org.sonar.scanner.report.AnalysisCachePublisher; | |||||
import org.sonar.scanner.report.AnalysisContextReportPublisher; | |||||
import org.sonar.scanner.report.AnalysisWarningsPublisher; | |||||
import org.sonar.scanner.report.CeTaskReportDataHolder; | |||||
import org.sonar.scanner.report.ChangedLinesPublisher; | |||||
import org.sonar.scanner.report.ComponentsPublisher; | |||||
import org.sonar.scanner.report.ContextPropertiesPublisher; | |||||
import org.sonar.scanner.report.JavaArchitectureInformationProvider; | |||||
import org.sonar.scanner.report.MetadataPublisher; | |||||
import org.sonar.scanner.report.ReportPublisher; | |||||
import org.sonar.scanner.report.ScannerFileStructureProvider; | |||||
import org.sonar.scanner.report.SourcePublisher; | |||||
import org.sonar.scanner.report.TestExecutionPublisher; | |||||
import org.sonar.scanner.repository.ContextPropertiesCache; | |||||
import org.sonar.scanner.repository.DefaultProjectRepositoriesLoader; | |||||
import org.sonar.scanner.repository.DefaultQualityProfileLoader; | |||||
import org.sonar.scanner.repository.ProjectRepositoriesProvider; | |||||
import org.sonar.scanner.repository.QualityProfilesProvider; | |||||
import org.sonar.scanner.repository.ReferenceBranchSupplier; | |||||
import org.sonar.scanner.repository.language.DefaultLanguagesRepository; | |||||
import org.sonar.scanner.repository.language.LanguagesRepository; | |||||
import org.sonar.scanner.repository.settings.DefaultProjectSettingsLoader; | |||||
import org.sonar.scanner.rule.ActiveRulesProvider; | |||||
import org.sonar.scanner.rule.DefaultActiveRulesLoader; | |||||
import org.sonar.scanner.rule.QProfileVerifier; | |||||
import org.sonar.scanner.scan.DeprecatedPropertiesWarningGenerator; | |||||
import org.sonar.scanner.scan.InputModuleHierarchyProvider; | |||||
import org.sonar.scanner.scan.InputProjectProvider; | |||||
import org.sonar.scanner.scan.ModuleIndexer; | |||||
import org.sonar.scanner.scan.MutableProjectReactorProvider; | |||||
import org.sonar.scanner.scan.MutableProjectSettings; | |||||
import org.sonar.scanner.scan.ProjectBuildersExecutor; | |||||
import org.sonar.scanner.scan.ProjectConfigurationProvider; | |||||
import org.sonar.scanner.scan.ProjectLock; | |||||
import org.sonar.scanner.scan.ProjectReactorBuilder; | |||||
import org.sonar.scanner.scan.ProjectReactorValidator; | |||||
import org.sonar.scanner.scan.ProjectServerSettingsProvider; | |||||
import org.sonar.scanner.scan.ScanProperties; | |||||
import org.sonar.scanner.scan.SonarGlobalPropertiesFilter; | |||||
import org.sonar.scanner.scan.SpringProjectScanContainer; | |||||
import org.sonar.scanner.scan.WorkDirectoriesInitializer; | |||||
import org.sonar.scanner.scan.branch.BranchConfiguration; | |||||
import org.sonar.scanner.scan.branch.BranchConfigurationProvider; | |||||
import org.sonar.scanner.scan.branch.BranchType; | |||||
import org.sonar.scanner.scan.branch.ProjectBranchesProvider; | |||||
import org.sonar.scanner.scan.filesystem.DefaultProjectFileSystem; | |||||
import org.sonar.scanner.scan.filesystem.FileIndexer; | |||||
import org.sonar.scanner.scan.filesystem.InputComponentStore; | |||||
import org.sonar.scanner.scan.filesystem.LanguageDetection; | |||||
import org.sonar.scanner.scan.filesystem.MetadataGenerator; | |||||
import org.sonar.scanner.scan.filesystem.ProjectCoverageAndDuplicationExclusions; | |||||
import org.sonar.scanner.scan.filesystem.ProjectExclusionFilters; | |||||
import org.sonar.scanner.scan.filesystem.ProjectFileIndexer; | |||||
import org.sonar.scanner.scan.filesystem.ScannerComponentIdGenerator; | |||||
import org.sonar.scanner.scan.filesystem.StatusDetection; | |||||
import org.sonar.scanner.scan.measure.DefaultMetricFinder; | |||||
import org.sonar.scanner.scm.ScmChangedFilesProvider; | |||||
import org.sonar.scanner.scm.ScmConfiguration; | |||||
import org.sonar.scanner.scm.ScmPublisher; | |||||
import org.sonar.scanner.scm.ScmRevisionImpl; | |||||
import org.sonar.scanner.sensor.DefaultSensorStorage; | |||||
import org.sonar.scanner.sensor.ExecutingSensorContext; | |||||
import org.sonar.scanner.sensor.ProjectSensorContext; | |||||
import org.sonar.scanner.sensor.ProjectSensorOptimizer; | |||||
import org.sonar.scanner.sensor.UnchangedFilesHandler; | |||||
import org.sonar.scm.git.GitScmSupport; | |||||
import org.sonar.scm.svn.SvnScmSupport; | |||||
import static org.sonar.api.batch.InstantiationStrategy.PER_BATCH; | |||||
import static org.sonar.core.extension.CoreExtensionsInstaller.noExtensionFilter; | |||||
import static org.sonar.scanner.bootstrap.ExtensionUtils.isDeprecatedScannerSide; | |||||
import static org.sonar.scanner.bootstrap.ExtensionUtils.isInstantiationStrategy; | |||||
import static org.sonar.scanner.bootstrap.ExtensionUtils.isScannerSide; | |||||
@Priority(3) | |||||
public class SpringScannerContainer extends SpringComponentContainer { | |||||
private static final Logger LOG = LoggerFactory.getLogger(SpringScannerContainer.class); | |||||
public SpringScannerContainer(SpringComponentContainer globalContainer) { | |||||
super(globalContainer); | |||||
} | |||||
@Override | |||||
protected void doBeforeStart() { | |||||
addScannerExtensions(); | |||||
addComponents(); | |||||
} | |||||
private void addScannerExtensions() { | |||||
getParentComponentByType(CoreExtensionsInstaller.class) | |||||
.install(this, noExtensionFilter(), extension -> getScannerProjectExtensionsFilter().accept(extension)); | |||||
getParentComponentByType(ExtensionInstaller.class) | |||||
.install(this, getScannerProjectExtensionsFilter()); | |||||
} | |||||
private void addComponents() { | |||||
add( | |||||
ScanProperties.class, | |||||
ProjectReactorBuilder.class, | |||||
WorkDirectoriesInitializer.class, | |||||
new MutableProjectReactorProvider(), | |||||
ProjectBuildersExecutor.class, | |||||
ProjectLock.class, | |||||
ProjectReactorValidator.class, | |||||
ProjectInfo.class, | |||||
new BranchConfigurationProvider(), | |||||
new ProjectBranchesProvider(), | |||||
ProjectRepositoriesProvider.class, | |||||
new ProjectServerSettingsProvider(), | |||||
AnalysisCacheEnabled.class, | |||||
DeprecatedPropertiesWarningGenerator.class, | |||||
// temp | |||||
new AnalysisTempFolderProvider(), | |||||
// file system | |||||
ModuleIndexer.class, | |||||
InputComponentStore.class, | |||||
PathResolver.class, | |||||
new InputProjectProvider(), | |||||
new InputModuleHierarchyProvider(), | |||||
ScannerComponentIdGenerator.class, | |||||
new ScmChangedFilesProvider(), | |||||
StatusDetection.class, | |||||
LanguageDetection.class, | |||||
MetadataGenerator.class, | |||||
FileMetadata.class, | |||||
FileIndexer.class, | |||||
ProjectFileIndexer.class, | |||||
ProjectExclusionFilters.class, | |||||
// rules | |||||
new ActiveRulesProvider(), | |||||
new QualityProfilesProvider(), | |||||
CheckFactory.class, | |||||
QProfileVerifier.class, | |||||
// issues | |||||
DefaultNoSonarFilter.class, | |||||
IssueFilters.class, | |||||
IssuePublisher.class, | |||||
// metrics | |||||
DefaultMetricFinder.class, | |||||
// issue exclusions | |||||
IssueInclusionPatternInitializer.class, | |||||
IssueExclusionPatternInitializer.class, | |||||
IssueExclusionsLoader.class, | |||||
EnforceIssuesFilter.class, | |||||
IgnoreIssuesFilter.class, | |||||
// context | |||||
ContextPropertiesCache.class, | |||||
SensorStrategy.class, | |||||
MutableProjectSettings.class, | |||||
SonarGlobalPropertiesFilter.class, | |||||
ProjectConfigurationProvider.class, | |||||
ProjectCoverageAndDuplicationExclusions.class, | |||||
// Plugin cache | |||||
AnalysisCacheProvider.class, | |||||
AnalysisCacheMemoryStorage.class, | |||||
DefaultAnalysisCacheLoader.class, | |||||
// Report | |||||
ReferenceBranchSupplier.class, | |||||
ScannerMetrics.class, | |||||
JavaArchitectureInformationProvider.class, | |||||
ReportPublisher.class, | |||||
ScannerFileStructureProvider.class, | |||||
AnalysisContextReportPublisher.class, | |||||
MetadataPublisher.class, | |||||
ActiveRulesPublisher.class, | |||||
ComponentsPublisher.class, | |||||
ContextPropertiesPublisher.class, | |||||
AnalysisCachePublisher.class, | |||||
TestExecutionPublisher.class, | |||||
SourcePublisher.class, | |||||
ChangedLinesPublisher.class, | |||||
AnalysisWarningsPublisher.class, | |||||
CeTaskReportDataHolder.class, | |||||
// Cpd | |||||
CpdExecutor.class, | |||||
CpdSettings.class, | |||||
SonarCpdBlockIndex.class, | |||||
// SCM | |||||
ScmConfiguration.class, | |||||
ScmPublisher.class, | |||||
ScmRevisionImpl.class, | |||||
// Sensors | |||||
DefaultSensorStorage.class, | |||||
DefaultFileLinesContextFactory.class, | |||||
ProjectSensorContext.class, | |||||
ProjectSensorOptimizer.class, | |||||
ExecutingSensorContext.class, | |||||
UnchangedFilesHandler.class, | |||||
// Filesystem | |||||
DefaultProjectFileSystem.class, | |||||
// CI | |||||
new CiConfigurationProvider(), | |||||
AppVeyor.class, | |||||
AwsCodeBuild.class, | |||||
AzureDevops.class, | |||||
Bamboo.class, | |||||
BitbucketPipelines.class, | |||||
Bitrise.class, | |||||
Buildkite.class, | |||||
CircleCi.class, | |||||
CirrusCi.class, | |||||
DroneCi.class, | |||||
GithubActions.class, | |||||
CodeMagic.class, | |||||
GitlabCi.class, | |||||
Jenkins.class, | |||||
SemaphoreCi.class, | |||||
TravisCi.class | |||||
); | |||||
add(GitScmSupport.getObjects()); | |||||
add(SvnScmSupport.getObjects()); | |||||
add(DefaultProjectSettingsLoader.class, | |||||
DefaultActiveRulesLoader.class, | |||||
DefaultQualityProfileLoader.class, | |||||
DefaultProjectRepositoriesLoader.class); | |||||
addIfMissing(DefaultLanguagesRepository.class, LanguagesRepository.class); | |||||
} | |||||
static ExtensionMatcher getScannerProjectExtensionsFilter() { | |||||
return extension -> { | |||||
if (isDeprecatedScannerSide(extension)) { | |||||
return isInstantiationStrategy(extension, PER_BATCH); | |||||
} | |||||
return isScannerSide(extension); | |||||
}; | |||||
} | |||||
@Override | |||||
protected void doAfterStart() { | |||||
ScanProperties properties = getComponentByType(ScanProperties.class); | |||||
properties.validate(); | |||||
properties.get("sonar.branch").ifPresent(deprecatedBranch -> { | |||||
throw MessageException.of("The 'sonar.branch' parameter is no longer supported. You should stop using it. " + | |||||
"Branch analysis is available in Developer Edition and above. See https://www.sonarsource.com/plans-and-pricing/developer/ for more information."); | |||||
}); | |||||
BranchConfiguration branchConfig = getComponentByType(BranchConfiguration.class); | |||||
if (branchConfig.branchType() == BranchType.PULL_REQUEST) { | |||||
LOG.info("Pull request {} for merge into {} from {}", branchConfig.pullRequestKey(), pullRequestBaseToDisplayName(branchConfig.targetBranchName()), | |||||
branchConfig.branchName()); | |||||
} else if (branchConfig.branchName() != null) { | |||||
LOG.info("Branch name: {}", branchConfig.branchName()); | |||||
} | |||||
getComponentByType(DeprecatedPropertiesWarningGenerator.class).execute(); | |||||
getComponentByType(ProjectFileIndexer.class).index(); | |||||
new SpringProjectScanContainer(this).execute(); | |||||
} | |||||
private static String pullRequestBaseToDisplayName(@Nullable String pullRequestBase) { | |||||
return pullRequestBase != null ? pullRequestBase : "default branch"; | |||||
} | |||||
} |
/* | |||||
* SonarQube | |||||
* Copyright (C) 2009-2023 SonarSource SA | |||||
* mailto:info AT sonarsource DOT com | |||||
* | |||||
* This program 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. | |||||
* | |||||
* This program 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.scanner.mediumtest; | |||||
import java.util.Collection; | |||||
import java.util.Comparator; | |||||
import java.util.HashMap; | |||||
import java.util.Map; | |||||
import javax.annotation.Priority; | |||||
import org.jetbrains.annotations.Nullable; | |||||
import org.sonar.api.resources.Languages; | |||||
import org.sonar.scanner.repository.language.Language; | |||||
import org.sonar.scanner.repository.language.LanguagesRepository; | |||||
import org.sonar.scanner.repository.language.SupportedLanguageDto; | |||||
@Priority(1) | |||||
public class FakeLanguagesRepository implements LanguagesRepository { | |||||
private final Map<String, Language> languageMap = new HashMap<>(); | |||||
public FakeLanguagesRepository() { | |||||
languageMap.put("xoo", new Language(new FakeLanguage("xoo", "xoo", new String[] { ".xoo" }, new String[0], true))); | |||||
} | |||||
public FakeLanguagesRepository(Languages languages) { | |||||
for (org.sonar.api.resources.Language language : languages.all()) { | |||||
languageMap.put(language.getKey(), new Language(new FakeLanguage(language.getKey(), language.getName(), language.getFileSuffixes(), language.filenamePatterns(), true))); | |||||
} | |||||
} | |||||
@Nullable | |||||
@Override | |||||
public Language get(String languageKey) { | |||||
return languageMap.get(languageKey); | |||||
} | |||||
@Override | |||||
public Collection<Language> all() { | |||||
return languageMap.values().stream() | |||||
// sorted for test consistency | |||||
.sorted(Comparator.comparing(Language::key)).toList(); | |||||
} | |||||
public void addLanguage(String key, String name, String[] suffixes, String[] filenamePatterns) { | |||||
languageMap.put(key, new Language(new FakeLanguage(key, name, suffixes, filenamePatterns, true))); | |||||
} | |||||
public void addLanguage(String key, String name, String[] suffixes, String[] filenamePatterns, boolean publishAllFiles) { | |||||
languageMap.put(key, new Language(new FakeLanguage(key, name, suffixes, filenamePatterns, publishAllFiles))); | |||||
} | |||||
private static class FakeLanguage extends SupportedLanguageDto { | |||||
private final boolean publishAllFiles; | |||||
public FakeLanguage(String key, String name, String[] fileSuffixes, String[] filenamePatterns, boolean publishAllFiles) { | |||||
super(key, name, fileSuffixes, filenamePatterns); | |||||
this.publishAllFiles = publishAllFiles; | |||||
} | |||||
@Override | |||||
public boolean publishAllFiles() { | |||||
return publishAllFiles; | |||||
} | |||||
} | |||||
} |
import java.util.HashMap; | import java.util.HashMap; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.Set; | |||||
import javax.annotation.Priority; | import javax.annotation.Priority; | ||||
import org.sonar.api.Plugin; | import org.sonar.api.Plugin; | ||||
import org.sonar.core.platform.PluginInfo; | import org.sonar.core.platform.PluginInfo; | ||||
} | } | ||||
@Override | @Override | ||||
public Map<String, ScannerPlugin> installRemotes() { | |||||
public Map<String, ScannerPlugin> installAllPlugins() { | |||||
return pluginsByKeys; | |||||
} | |||||
@Override | |||||
public Map<String, ScannerPlugin> installRequiredPlugins() { | |||||
return pluginsByKeys; | |||||
} | |||||
@Override | |||||
public Map<String, ScannerPlugin> installPluginsForLanguages(Set<String> languageKeys) { | |||||
return pluginsByKeys; | return pluginsByKeys; | ||||
} | } | ||||
*/ | */ | ||||
package org.sonar.scanner.repository.language; | package org.sonar.scanner.repository.language; | ||||
import java.util.Arrays; | |||||
import com.google.gson.Gson; | |||||
import java.io.Reader; | |||||
import java.util.Collection; | import java.util.Collection; | ||||
import java.util.HashMap; | |||||
import java.util.List; | |||||
import java.util.Map; | |||||
import java.util.function.Function; | |||||
import java.util.stream.Collectors; | |||||
import javax.annotation.CheckForNull; | import javax.annotation.CheckForNull; | ||||
import javax.annotation.concurrent.Immutable; | |||||
import org.slf4j.Logger; | |||||
import org.slf4j.LoggerFactory; | |||||
import org.sonar.api.Startable; | import org.sonar.api.Startable; | ||||
import org.sonar.api.config.Configuration; | |||||
import org.sonar.api.resources.Languages; | import org.sonar.api.resources.Languages; | ||||
import org.sonar.scanner.bootstrap.DefaultScannerWsClient; | |||||
import org.sonarqube.ws.client.GetRequest; | |||||
/** | /** | ||||
* Languages repository using {@link Languages} | * Languages repository using {@link Languages} | ||||
* @since 4.4 | * @since 4.4 | ||||
*/ | */ | ||||
@Immutable | |||||
public class DefaultLanguagesRepository implements LanguagesRepository, Startable { | public class DefaultLanguagesRepository implements LanguagesRepository, Startable { | ||||
private static final Logger LOG = LoggerFactory.getLogger(DefaultLanguagesRepository.class); | |||||
private static final String LANGUAGES_WS_URL = "/api/languages/list"; | |||||
private static final Map<String, String> PROPERTY_FRAGMENT_MAP = Map.of( | |||||
"js", "javascript", | |||||
"ts", "typescript", | |||||
"py", "python", | |||||
"web", "html" | |||||
); | |||||
private final Languages languages; | |||||
private final Map<String, Language> languages = new HashMap<>(); | |||||
private final DefaultScannerWsClient wsClient; | |||||
private final Configuration properties; | |||||
public DefaultLanguagesRepository(Languages languages) { | |||||
this.languages = languages; | |||||
public DefaultLanguagesRepository(DefaultScannerWsClient wsClient, Configuration properties) { | |||||
this.wsClient = wsClient; | |||||
this.properties = properties; | |||||
} | } | ||||
@Override | @Override | ||||
public void start() { | public void start() { | ||||
if (languages.all().length == 0) { | |||||
throw new IllegalStateException("No language plugins are installed."); | |||||
GetRequest getRequest = new GetRequest(LANGUAGES_WS_URL); | |||||
LanguagesWSResponse response; | |||||
try (Reader reader = wsClient.call(getRequest).contentReader()) { | |||||
response = new Gson().fromJson(reader, LanguagesWSResponse.class); | |||||
} catch (Exception e) { | |||||
throw new IllegalStateException("Fail to parse response of " + LANGUAGES_WS_URL, e); | |||||
} | } | ||||
languages.putAll(response.languages.stream() | |||||
.map(this::populateFileSuffixesAndPatterns) | |||||
.collect(Collectors.toMap(Language::key, Function.identity()))); | |||||
} | |||||
private Language populateFileSuffixesAndPatterns(SupportedLanguageDto lang) { | |||||
String propertyFragment = PROPERTY_FRAGMENT_MAP.getOrDefault(lang.getKey(), lang.getKey()); | |||||
lang.setFileSuffixes(properties.getStringArray(String.format("sonar.%s.file.suffixes", propertyFragment))); | |||||
lang.setFilenamePatterns(properties.getStringArray(String.format("sonar.%s.file.patterns", propertyFragment))); | |||||
if (lang.filenamePatterns() == null && lang.getFileSuffixes() == null) { | |||||
LOG.debug("Language '{}' cannot be detected as it has neither suffixes nor patterns.", lang.getName()); | |||||
} | |||||
return new Language(lang); | |||||
} | |||||
private String[] getFileSuffixes(String languageKey) { | |||||
String propName = String.format("sonar.%s.file.suffixes", PROPERTY_FRAGMENT_MAP.getOrDefault(languageKey, languageKey)); | |||||
return properties.getStringArray(propName); | |||||
} | } | ||||
/** | /** | ||||
@Override | @Override | ||||
@CheckForNull | @CheckForNull | ||||
public Language get(String languageKey) { | public Language get(String languageKey) { | ||||
org.sonar.api.resources.Language language = languages.get(languageKey); | |||||
return language != null ? new Language(language) : null; | |||||
return languages.get(languageKey); | |||||
} | } | ||||
/** | /** | ||||
*/ | */ | ||||
@Override | @Override | ||||
public Collection<Language> all() { | public Collection<Language> all() { | ||||
return Arrays.stream(languages.all()) | |||||
.map(Language::new) | |||||
.toList(); | |||||
return languages.values(); | |||||
} | } | ||||
@Override | @Override | ||||
// nothing to do | // nothing to do | ||||
} | } | ||||
private static class LanguagesWSResponse { | |||||
List<SupportedLanguageDto> languages; | |||||
} | |||||
} | } |
/* | |||||
* SonarQube | |||||
* Copyright (C) 2009-2024 SonarSource SA | |||||
* mailto:info AT sonarsource DOT com | |||||
* | |||||
* This program 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. | |||||
* | |||||
* This program 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.scanner.repository.language; | |||||
public class SupportedLanguageDto implements org.sonar.api.resources.Language { | |||||
private String key; | |||||
private String name; | |||||
private String[] fileSuffixes; | |||||
private String[] filenamePatterns; | |||||
public SupportedLanguageDto(String key, String name, String[] fileSuffixes, String[] filenamePatterns) { | |||||
this.key = key; | |||||
this.name = name; | |||||
this.fileSuffixes = fileSuffixes; | |||||
this.filenamePatterns = filenamePatterns; | |||||
} | |||||
@Override | |||||
public String getKey() { | |||||
return key; | |||||
} | |||||
@Override | |||||
public String getName() { | |||||
return name; | |||||
} | |||||
@Override | |||||
public String[] getFileSuffixes() { | |||||
return fileSuffixes; | |||||
} | |||||
public void setFileSuffixes(String[] fileSuffixes) { | |||||
this.fileSuffixes = fileSuffixes; | |||||
} | |||||
@Override | |||||
public String[] filenamePatterns() { | |||||
return filenamePatterns; | |||||
} | |||||
public void setFilenamePatterns(String[] filenamePatterns) { | |||||
this.filenamePatterns = filenamePatterns; | |||||
} | |||||
} |
static Map<String, String> toMap(List<Settings.Setting> settingsList) { | static Map<String, String> toMap(List<Settings.Setting> settingsList) { | ||||
Map<String, String> result = new LinkedHashMap<>(); | Map<String, String> result = new LinkedHashMap<>(); | ||||
for (Settings.Setting s : settingsList) { | for (Settings.Setting s : settingsList) { | ||||
if (!s.getInherited()) { | |||||
// we need the "*.file.suffixes" and "*.file.patterns" properties for language detection | |||||
// see DefaultLanguagesRepository.populateFileSuffixesAndPatterns() | |||||
if (!s.getInherited() || s.getKey().endsWith(".file.suffixes") || s.getKey().endsWith(".file.patterns")) { | |||||
switch (s.getValueOneOfCase()) { | switch (s.getValueOneOfCase()) { | ||||
case VALUE: | case VALUE: | ||||
result.put(s.getKey(), s.getValue()); | result.put(s.getKey(), s.getValue()); |
*/ | */ | ||||
package org.sonar.scanner.scan; | package org.sonar.scanner.scan; | ||||
import javax.annotation.Nullable; | |||||
import java.util.Collection; | |||||
import java.util.Set; | |||||
import javax.annotation.Priority; | import javax.annotation.Priority; | ||||
import org.slf4j.Logger; | import org.slf4j.Logger; | ||||
import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||
import org.sonar.api.Plugin; | |||||
import org.sonar.api.batch.fs.internal.DefaultInputModule; | import org.sonar.api.batch.fs.internal.DefaultInputModule; | ||||
import org.sonar.api.batch.fs.internal.FileMetadata; | |||||
import org.sonar.api.batch.fs.internal.SensorStrategy; | |||||
import org.sonar.api.batch.rule.CheckFactory; | |||||
import org.sonar.api.batch.sensor.issue.internal.DefaultNoSonarFilter; | |||||
import org.sonar.api.measures.Metrics; | |||||
import org.sonar.api.resources.Languages; | |||||
import org.sonar.api.resources.ResourceTypes; | import org.sonar.api.resources.ResourceTypes; | ||||
import org.sonar.api.scan.filesystem.PathResolver; | |||||
import org.sonar.api.utils.MessageException; | |||||
import org.sonar.core.config.ScannerProperties; | import org.sonar.core.config.ScannerProperties; | ||||
import org.sonar.core.extension.CoreExtensionsInstaller; | |||||
import org.sonar.core.language.LanguagesProvider; | import org.sonar.core.language.LanguagesProvider; | ||||
import org.sonar.core.metric.ScannerMetrics; | import org.sonar.core.metric.ScannerMetrics; | ||||
import org.sonar.core.platform.PluginInfo; | |||||
import org.sonar.core.platform.SpringComponentContainer; | import org.sonar.core.platform.SpringComponentContainer; | ||||
import org.sonar.scanner.DefaultFileLinesContextFactory; | |||||
import org.sonar.scanner.ProjectInfo; | |||||
import org.sonar.scanner.analysis.AnalysisTempFolderProvider; | |||||
import org.sonar.scanner.bootstrap.ExtensionInstaller; | import org.sonar.scanner.bootstrap.ExtensionInstaller; | ||||
import org.sonar.scanner.bootstrap.ExtensionMatcher; | import org.sonar.scanner.bootstrap.ExtensionMatcher; | ||||
import org.sonar.scanner.bootstrap.GlobalAnalysisMode; | import org.sonar.scanner.bootstrap.GlobalAnalysisMode; | ||||
import org.sonar.scanner.bootstrap.PostJobExtensionDictionary; | import org.sonar.scanner.bootstrap.PostJobExtensionDictionary; | ||||
import org.sonar.scanner.cache.AnalysisCacheEnabled; | |||||
import org.sonar.scanner.cache.AnalysisCacheMemoryStorage; | |||||
import org.sonar.scanner.cache.AnalysisCacheProvider; | |||||
import org.sonar.scanner.cache.DefaultAnalysisCacheLoader; | |||||
import org.sonar.scanner.ci.CiConfigurationProvider; | |||||
import org.sonar.scanner.ci.vendors.AppVeyor; | |||||
import org.sonar.scanner.ci.vendors.AwsCodeBuild; | |||||
import org.sonar.scanner.ci.vendors.AzureDevops; | |||||
import org.sonar.scanner.ci.vendors.Bamboo; | |||||
import org.sonar.scanner.ci.vendors.BitbucketPipelines; | |||||
import org.sonar.scanner.ci.vendors.Bitrise; | |||||
import org.sonar.scanner.ci.vendors.Buildkite; | |||||
import org.sonar.scanner.ci.vendors.CircleCi; | |||||
import org.sonar.scanner.ci.vendors.CirrusCi; | |||||
import org.sonar.scanner.ci.vendors.CodeMagic; | |||||
import org.sonar.scanner.ci.vendors.DroneCi; | |||||
import org.sonar.scanner.ci.vendors.GithubActions; | |||||
import org.sonar.scanner.ci.vendors.GitlabCi; | |||||
import org.sonar.scanner.ci.vendors.Jenkins; | |||||
import org.sonar.scanner.ci.vendors.SemaphoreCi; | |||||
import org.sonar.scanner.ci.vendors.TravisCi; | |||||
import org.sonar.scanner.bootstrap.ScannerPluginRepository; | |||||
import org.sonar.scanner.cpd.CpdExecutor; | import org.sonar.scanner.cpd.CpdExecutor; | ||||
import org.sonar.scanner.cpd.CpdSettings; | |||||
import org.sonar.scanner.cpd.index.SonarCpdBlockIndex; | |||||
import org.sonar.scanner.fs.InputModuleHierarchy; | import org.sonar.scanner.fs.InputModuleHierarchy; | ||||
import org.sonar.scanner.issue.IssueFilters; | |||||
import org.sonar.scanner.issue.IssuePublisher; | |||||
import org.sonar.scanner.issue.ignore.EnforceIssuesFilter; | |||||
import org.sonar.scanner.issue.ignore.IgnoreIssuesFilter; | |||||
import org.sonar.scanner.issue.ignore.pattern.IssueExclusionPatternInitializer; | |||||
import org.sonar.scanner.issue.ignore.pattern.IssueInclusionPatternInitializer; | |||||
import org.sonar.scanner.issue.ignore.scanner.IssueExclusionsLoader; | |||||
import org.sonar.scanner.mediumtest.AnalysisObservers; | import org.sonar.scanner.mediumtest.AnalysisObservers; | ||||
import org.sonar.scanner.postjob.DefaultPostJobContext; | import org.sonar.scanner.postjob.DefaultPostJobContext; | ||||
import org.sonar.scanner.postjob.PostJobOptimizer; | import org.sonar.scanner.postjob.PostJobOptimizer; | ||||
import org.sonar.scanner.postjob.PostJobsExecutor; | import org.sonar.scanner.postjob.PostJobsExecutor; | ||||
import org.sonar.scanner.qualitygate.QualityGateCheck; | import org.sonar.scanner.qualitygate.QualityGateCheck; | ||||
import org.sonar.scanner.report.ActiveRulesPublisher; | |||||
import org.sonar.scanner.report.AnalysisCachePublisher; | |||||
import org.sonar.scanner.report.AnalysisContextReportPublisher; | |||||
import org.sonar.scanner.report.AnalysisWarningsPublisher; | |||||
import org.sonar.scanner.report.CeTaskReportDataHolder; | |||||
import org.sonar.scanner.report.ChangedLinesPublisher; | |||||
import org.sonar.scanner.report.ComponentsPublisher; | |||||
import org.sonar.scanner.report.ContextPropertiesPublisher; | |||||
import org.sonar.scanner.report.JavaArchitectureInformationProvider; | |||||
import org.sonar.scanner.report.MetadataPublisher; | |||||
import org.sonar.scanner.report.ReportPublisher; | import org.sonar.scanner.report.ReportPublisher; | ||||
import org.sonar.scanner.report.ScannerFileStructureProvider; | |||||
import org.sonar.scanner.report.SourcePublisher; | |||||
import org.sonar.scanner.report.TestExecutionPublisher; | |||||
import org.sonar.scanner.repository.ContextPropertiesCache; | |||||
import org.sonar.scanner.repository.DefaultProjectRepositoriesLoader; | |||||
import org.sonar.scanner.repository.DefaultQualityProfileLoader; | |||||
import org.sonar.scanner.repository.ProjectRepositoriesProvider; | |||||
import org.sonar.scanner.repository.QualityProfilesProvider; | |||||
import org.sonar.scanner.repository.ReferenceBranchSupplier; | |||||
import org.sonar.scanner.repository.language.DefaultLanguagesRepository; | |||||
import org.sonar.scanner.repository.settings.DefaultProjectSettingsLoader; | |||||
import org.sonar.scanner.rule.ActiveRulesProvider; | |||||
import org.sonar.scanner.rule.DefaultActiveRulesLoader; | |||||
import org.sonar.scanner.rule.QProfileVerifier; | import org.sonar.scanner.rule.QProfileVerifier; | ||||
import org.sonar.scanner.scan.branch.BranchConfiguration; | |||||
import org.sonar.scanner.scan.branch.BranchConfigurationProvider; | |||||
import org.sonar.scanner.scan.branch.BranchType; | |||||
import org.sonar.scanner.scan.branch.ProjectBranchesProvider; | |||||
import org.sonar.scanner.scan.filesystem.DefaultProjectFileSystem; | |||||
import org.sonar.scanner.scan.filesystem.FileIndexer; | |||||
import org.sonar.scanner.scan.filesystem.InputComponentStore; | import org.sonar.scanner.scan.filesystem.InputComponentStore; | ||||
import org.sonar.scanner.scan.filesystem.LanguageDetection; | |||||
import org.sonar.scanner.scan.filesystem.MetadataGenerator; | |||||
import org.sonar.scanner.scan.filesystem.ProjectCoverageAndDuplicationExclusions; | |||||
import org.sonar.scanner.scan.filesystem.ProjectExclusionFilters; | |||||
import org.sonar.scanner.scan.filesystem.ProjectFileIndexer; | |||||
import org.sonar.scanner.scan.filesystem.ScannerComponentIdGenerator; | |||||
import org.sonar.scanner.scan.filesystem.StatusDetection; | |||||
import org.sonar.scanner.scan.measure.DefaultMetricFinder; | |||||
import org.sonar.scanner.scm.ScmChangedFilesProvider; | |||||
import org.sonar.scanner.scm.ScmConfiguration; | |||||
import org.sonar.scanner.scm.ScmPublisher; | import org.sonar.scanner.scm.ScmPublisher; | ||||
import org.sonar.scanner.scm.ScmRevisionImpl; | |||||
import org.sonar.scanner.sensor.DefaultSensorStorage; | |||||
import org.sonar.scanner.sensor.ExecutingSensorContext; | |||||
import org.sonar.scanner.sensor.ProjectSensorContext; | |||||
import org.sonar.scanner.sensor.ProjectSensorExtensionDictionary; | import org.sonar.scanner.sensor.ProjectSensorExtensionDictionary; | ||||
import org.sonar.scanner.sensor.ProjectSensorOptimizer; | |||||
import org.sonar.scanner.sensor.ProjectSensorsExecutor; | import org.sonar.scanner.sensor.ProjectSensorsExecutor; | ||||
import org.sonar.scanner.sensor.UnchangedFilesHandler; | |||||
import org.sonar.scm.git.GitScmSupport; | |||||
import org.sonar.scm.svn.SvnScmSupport; | |||||
import static org.sonar.api.batch.InstantiationStrategy.PER_BATCH; | import static org.sonar.api.batch.InstantiationStrategy.PER_BATCH; | ||||
import static org.sonar.api.utils.Preconditions.checkNotNull; | |||||
import static org.sonar.core.extension.CoreExtensionsInstaller.noExtensionFilter; | |||||
import static org.sonar.scanner.bootstrap.ExtensionUtils.isDeprecatedScannerSide; | import static org.sonar.scanner.bootstrap.ExtensionUtils.isDeprecatedScannerSide; | ||||
import static org.sonar.scanner.bootstrap.ExtensionUtils.isInstantiationStrategy; | import static org.sonar.scanner.bootstrap.ExtensionUtils.isInstantiationStrategy; | ||||
import static org.sonar.scanner.bootstrap.ExtensionUtils.isScannerSide; | import static org.sonar.scanner.bootstrap.ExtensionUtils.isScannerSide; | ||||
public class SpringProjectScanContainer extends SpringComponentContainer { | public class SpringProjectScanContainer extends SpringComponentContainer { | ||||
private static final Logger LOG = LoggerFactory.getLogger(SpringProjectScanContainer.class); | private static final Logger LOG = LoggerFactory.getLogger(SpringProjectScanContainer.class); | ||||
public SpringProjectScanContainer(SpringComponentContainer globalContainer) { | |||||
super(globalContainer); | |||||
public SpringProjectScanContainer(SpringComponentContainer parentContainer) { | |||||
super(parentContainer); | |||||
} | } | ||||
@Override | @Override | ||||
protected void doBeforeStart() { | protected void doBeforeStart() { | ||||
addScannerExtensions(); | |||||
Set<String> languages = getParentComponentByType(InputComponentStore.class).languages(); | |||||
installPluginsForLanguages(languages); | |||||
addScannerComponents(); | addScannerComponents(); | ||||
} | } | ||||
private void installPluginsForLanguages(Set<String> languageKeys) { | |||||
ScannerPluginRepository pluginRepository = getParentComponentByType(ScannerPluginRepository.class); | |||||
Collection<PluginInfo> languagePlugins = pluginRepository.installPluginsForLanguages(languageKeys); | |||||
for (PluginInfo pluginInfo : languagePlugins) { | |||||
Plugin instance = pluginRepository.getPluginInstance(pluginInfo.getKey()); | |||||
addExtension(pluginInfo, instance); | |||||
} | |||||
getParentComponentByType(ExtensionInstaller.class) | |||||
.installExtensionsForPlugins(this, getScannerProjectExtensionsFilter(), languagePlugins); | |||||
} | |||||
private void addScannerComponents() { | private void addScannerComponents() { | ||||
add( | add( | ||||
ScanProperties.class, | |||||
ProjectReactorBuilder.class, | |||||
WorkDirectoriesInitializer.class, | |||||
new MutableProjectReactorProvider(), | |||||
ProjectBuildersExecutor.class, | |||||
ProjectLock.class, | |||||
ResourceTypes.class, | |||||
ProjectReactorValidator.class, | |||||
ProjectInfo.class, | |||||
new BranchConfigurationProvider(), | |||||
new ProjectBranchesProvider(), | |||||
ProjectRepositoriesProvider.class, | |||||
new ProjectServerSettingsProvider(), | |||||
AnalysisCacheEnabled.class, | |||||
DeprecatedPropertiesWarningGenerator.class, | |||||
// temp | |||||
new AnalysisTempFolderProvider(), | |||||
// file system | |||||
ModuleIndexer.class, | |||||
InputComponentStore.class, | |||||
PathResolver.class, | |||||
new InputProjectProvider(), | |||||
new InputModuleHierarchyProvider(), | |||||
ScannerComponentIdGenerator.class, | |||||
new ScmChangedFilesProvider(), | |||||
StatusDetection.class, | |||||
LanguageDetection.class, | |||||
MetadataGenerator.class, | |||||
FileMetadata.class, | |||||
FileIndexer.class, | |||||
ProjectFileIndexer.class, | |||||
ProjectExclusionFilters.class, | |||||
// rules | |||||
new ActiveRulesProvider(), | |||||
new QualityProfilesProvider(), | |||||
CheckFactory.class, | |||||
QProfileVerifier.class, | |||||
// issues | |||||
DefaultNoSonarFilter.class, | |||||
IssueFilters.class, | |||||
IssuePublisher.class, | |||||
// metrics | |||||
DefaultMetricFinder.class, | |||||
ResourceTypes.class, | |||||
// lang | // lang | ||||
LanguagesProvider.class, | LanguagesProvider.class, | ||||
DefaultLanguagesRepository.class, | |||||
// issue exclusions | |||||
IssueInclusionPatternInitializer.class, | |||||
IssueExclusionPatternInitializer.class, | |||||
IssueExclusionsLoader.class, | |||||
EnforceIssuesFilter.class, | |||||
IgnoreIssuesFilter.class, | |||||
// context | |||||
ContextPropertiesCache.class, | |||||
ContextPropertiesPublisher.class, | |||||
// rules | |||||
QProfileVerifier.class, | |||||
SensorStrategy.class, | |||||
MutableProjectSettings.class, | |||||
ScannerProperties.class, | ScannerProperties.class, | ||||
SonarGlobalPropertiesFilter.class, | |||||
ProjectConfigurationProvider.class, | |||||
ProjectCoverageAndDuplicationExclusions.class, | |||||
// Plugin cache | |||||
AnalysisCacheProvider.class, | |||||
AnalysisCacheMemoryStorage.class, | |||||
DefaultAnalysisCacheLoader.class, | |||||
// Report | |||||
ReferenceBranchSupplier.class, | |||||
ScannerMetrics.class, | |||||
JavaArchitectureInformationProvider.class, | |||||
ReportPublisher.class, | |||||
ScannerFileStructureProvider.class, | |||||
AnalysisContextReportPublisher.class, | |||||
MetadataPublisher.class, | |||||
ActiveRulesPublisher.class, | |||||
ComponentsPublisher.class, | |||||
AnalysisCachePublisher.class, | |||||
TestExecutionPublisher.class, | |||||
SourcePublisher.class, | |||||
ChangedLinesPublisher.class, | |||||
AnalysisWarningsPublisher.class, | |||||
CeTaskReportDataHolder.class, | |||||
// QualityGate check | // QualityGate check | ||||
QualityGateCheck.class, | QualityGateCheck.class, | ||||
// Cpd | |||||
CpdExecutor.class, | |||||
CpdSettings.class, | |||||
SonarCpdBlockIndex.class, | |||||
// PostJobs | // PostJobs | ||||
PostJobsExecutor.class, | PostJobsExecutor.class, | ||||
PostJobOptimizer.class, | PostJobOptimizer.class, | ||||
DefaultPostJobContext.class, | DefaultPostJobContext.class, | ||||
PostJobExtensionDictionary.class, | PostJobExtensionDictionary.class, | ||||
// SCM | |||||
ScmConfiguration.class, | |||||
ScmPublisher.class, | |||||
ScmRevisionImpl.class, | |||||
// Sensors | // Sensors | ||||
DefaultSensorStorage.class, | |||||
DefaultFileLinesContextFactory.class, | |||||
ProjectSensorContext.class, | |||||
ProjectSensorOptimizer.class, | |||||
ProjectSensorsExecutor.class, | |||||
ExecutingSensorContext.class, | |||||
ProjectSensorExtensionDictionary.class, | ProjectSensorExtensionDictionary.class, | ||||
UnchangedFilesHandler.class, | |||||
// Filesystem | |||||
DefaultProjectFileSystem.class, | |||||
// CI | |||||
new CiConfigurationProvider(), | |||||
AppVeyor.class, | |||||
AwsCodeBuild.class, | |||||
AzureDevops.class, | |||||
Bamboo.class, | |||||
BitbucketPipelines.class, | |||||
Bitrise.class, | |||||
Buildkite.class, | |||||
CircleCi.class, | |||||
CirrusCi.class, | |||||
DroneCi.class, | |||||
GithubActions.class, | |||||
CodeMagic.class, | |||||
GitlabCi.class, | |||||
Jenkins.class, | |||||
SemaphoreCi.class, | |||||
TravisCi.class, | |||||
ProjectSensorsExecutor.class, | |||||
AnalysisObservers.class); | AnalysisObservers.class); | ||||
add(GitScmSupport.getObjects()); | |||||
add(SvnScmSupport.getObjects()); | |||||
add(DefaultProjectSettingsLoader.class, | |||||
DefaultActiveRulesLoader.class, | |||||
DefaultQualityProfileLoader.class, | |||||
DefaultProjectRepositoriesLoader.class); | |||||
} | |||||
private void addScannerExtensions() { | |||||
checkNotNull(getParent()); | |||||
getParent().getComponentByType(CoreExtensionsInstaller.class) | |||||
.install(this, noExtensionFilter(), extension -> getScannerProjectExtensionsFilter().accept(extension)); | |||||
getParent().getComponentByType(ExtensionInstaller.class) | |||||
.install(this, getScannerProjectExtensionsFilter()); | |||||
} | } | ||||
static ExtensionMatcher getScannerProjectExtensionsFilter() { | static ExtensionMatcher getScannerProjectExtensionsFilter() { | ||||
@Override | @Override | ||||
protected void doAfterStart() { | protected void doAfterStart() { | ||||
getParentComponentByType(ScannerMetrics.class).addPluginMetrics(getComponentsByType(Metrics.class)); | |||||
getComponentByType(ProjectLock.class).tryLock(); | getComponentByType(ProjectLock.class).tryLock(); | ||||
GlobalAnalysisMode analysisMode = getComponentByType(GlobalAnalysisMode.class); | GlobalAnalysisMode analysisMode = getComponentByType(GlobalAnalysisMode.class); | ||||
InputModuleHierarchy tree = getComponentByType(InputModuleHierarchy.class); | InputModuleHierarchy tree = getComponentByType(InputModuleHierarchy.class); | ||||
ScanProperties properties = getComponentByType(ScanProperties.class); | ScanProperties properties = getComponentByType(ScanProperties.class); | ||||
properties.validate(); | |||||
properties.get("sonar.branch").ifPresent(deprecatedBranch -> { | |||||
throw MessageException.of("The 'sonar.branch' parameter is no longer supported. You should stop using it. " + | |||||
"Branch analysis is available in Developer Edition and above. See https://www.sonarsource.com/plans-and-pricing/developer/ for more information."); | |||||
}); | |||||
BranchConfiguration branchConfig = getComponentByType(BranchConfiguration.class); | |||||
if (branchConfig.branchType() == BranchType.PULL_REQUEST) { | |||||
LOG.info("Pull request {} for merge into {} from {}", branchConfig.pullRequestKey(), pullRequestBaseToDisplayName(branchConfig.targetBranchName()), | |||||
branchConfig.branchName()); | |||||
} else if (branchConfig.branchName() != null) { | |||||
LOG.info("Branch name: {}", branchConfig.branchName()); | |||||
if (getComponentByType(Languages.class).all().length == 0) { | |||||
throw new IllegalStateException("No language plugins are installed."); | |||||
} | } | ||||
getComponentByType(DeprecatedPropertiesWarningGenerator.class).execute(); | |||||
getComponentByType(ProjectFileIndexer.class).index(); | |||||
// Log detected languages and their profiles after FS is indexed and languages detected | // Log detected languages and their profiles after FS is indexed and languages detected | ||||
getComponentByType(QProfileVerifier.class).execute(); | getComponentByType(QProfileVerifier.class).execute(); | ||||
} | } | ||||
} | } | ||||
private static String pullRequestBaseToDisplayName(@Nullable String pullRequestBase) { | |||||
return pullRequestBase != null ? pullRequestBase : "default branch"; | |||||
} | |||||
private void scanRecursively(InputModuleHierarchy tree, DefaultInputModule module) { | private void scanRecursively(InputModuleHierarchy tree, DefaultInputModule module) { | ||||
for (DefaultInputModule child : tree.children(module)) { | for (DefaultInputModule child : tree.children(module)) { | ||||
scanRecursively(tree, child); | scanRecursively(tree, child); |
import java.io.IOException; | import java.io.IOException; | ||||
import java.io.InputStreamReader; | import java.io.InputStreamReader; | ||||
import java.io.StringReader; | import java.io.StringReader; | ||||
import java.util.HashSet; | |||||
import java.util.List; | |||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.Optional; | import java.util.Optional; | ||||
import java.util.jar.Attributes; | import java.util.jar.Attributes; | ||||
@Test | @Test | ||||
public void download_installed_plugins() throws IOException { | public void download_installed_plugins() throws IOException { | ||||
WsTestUtil.mockReader(wsClient, "api/plugins/installed", new InputStreamReader(getClass().getResourceAsStream("ScannerPluginInstallerTest/installed-plugins-ws.json"))); | |||||
WsTestUtil.mockReader(wsClient, "api/plugins/installed", | |||||
new InputStreamReader(getClass().getResourceAsStream("ScannerPluginInstallerTest/installed-plugins-ws.json"))); | |||||
enqueueDownload("scmgit", "abc"); | |||||
enqueueDownload("java", "def"); | |||||
Map<String, ScannerPlugin> result = underTest.installRequiredPlugins(); | |||||
assertThat(result.keySet()).containsExactlyInAnyOrder("scmgit"); | |||||
ScannerPlugin gitPlugin = result.get("scmgit"); | |||||
assertThat(gitPlugin.getKey()).isEqualTo("scmgit"); | |||||
assertThat(gitPlugin.getInfo().getNonNullJarFile()).exists().isFile(); | |||||
assertThat(gitPlugin.getUpdatedAt()).isEqualTo(100L); | |||||
Map<String, ScannerPlugin> result2 = underTest.installPluginsForLanguages(new HashSet<>(List.of("java"))); | |||||
assertThat(result2.keySet()).containsExactlyInAnyOrder("java"); | |||||
ScannerPlugin javaPlugin = result2.get("java"); | |||||
assertThat(javaPlugin.getKey()).isEqualTo("java"); | |||||
assertThat(javaPlugin.getInfo().getNonNullJarFile()).exists().isFile(); | |||||
assertThat(javaPlugin.getUpdatedAt()).isEqualTo(200L); | |||||
} | |||||
@Test | |||||
public void download_all_plugins() throws IOException { | |||||
WsTestUtil.mockReader(wsClient, "api/plugins/installed", | |||||
new InputStreamReader(getClass().getResourceAsStream("ScannerPluginInstallerTest/installed-plugins-ws.json"))); | |||||
enqueueDownload("scmgit", "abc"); | enqueueDownload("scmgit", "abc"); | ||||
enqueueDownload("java", "def"); | enqueueDownload("java", "def"); | ||||
Map<String, ScannerPlugin> result = underTest.installRemotes(); | |||||
Map<String, ScannerPlugin> result = underTest.installAllPlugins(); | |||||
assertThat(result.keySet()).containsExactlyInAnyOrder("scmgit", "java"); | assertThat(result.keySet()).containsExactlyInAnyOrder("scmgit", "java"); | ||||
ScannerPlugin gitPlugin = result.get("scmgit"); | ScannerPlugin gitPlugin = result.get("scmgit"); | ||||
public void fail_if_json_of_installed_plugins_is_not_valid() { | public void fail_if_json_of_installed_plugins_is_not_valid() { | ||||
WsTestUtil.mockReader(wsClient, "api/plugins/installed", new StringReader("not json")); | WsTestUtil.mockReader(wsClient, "api/plugins/installed", new StringReader("not json")); | ||||
assertThatThrownBy(() -> underTest.installRemotes()) | |||||
assertThatThrownBy(() -> underTest.installRequiredPlugins()) | |||||
.isInstanceOf(IllegalStateException.class) | .isInstanceOf(IllegalStateException.class) | ||||
.hasMessage("Fail to parse response of api/plugins/installed"); | .hasMessage("Fail to parse response of api/plugins/installed"); | ||||
} | } | ||||
enqueueDownload("java", "def"); | enqueueDownload("java", "def"); | ||||
enqueueDownload("cobol", "ghi"); | enqueueDownload("cobol", "ghi"); | ||||
Map<String, ScannerPlugin> result = underTest.installRemotes(); | |||||
Map<String, ScannerPlugin> result = underTest.installRequiredPlugins(); | |||||
assertThat(result.keySet()).containsExactlyInAnyOrder("java", "cobol"); | assertThat(result.keySet()).containsExactlyInAnyOrder("java", "cobol"); | ||||
} | } | ||||
enqueueDownload("cobol", "ghi"); | enqueueDownload("cobol", "ghi"); | ||||
enqueueNotFoundDownload("java", "def"); | enqueueNotFoundDownload("java", "def"); | ||||
assertThatThrownBy(() -> underTest.installRemotes()) | |||||
assertThatThrownBy(() -> underTest.installRequiredPlugins()) | |||||
.isInstanceOf(IllegalStateException.class) | .isInstanceOf(IllegalStateException.class) | ||||
.hasMessage("Fail to download plugin [java]. Not found."); | .hasMessage("Fail to download plugin [java]. Not found."); | ||||
} | } |
import com.google.common.collect.ImmutableMap; | import com.google.common.collect.ImmutableMap; | ||||
import java.io.File; | import java.io.File; | ||||
import java.util.Collection; | |||||
import java.util.Collections; | import java.util.Collections; | ||||
import java.util.HashSet; | |||||
import java.util.List; | |||||
import java.util.Map; | |||||
import java.util.Optional; | |||||
import java.util.Set; | |||||
import org.junit.Test; | import org.junit.Test; | ||||
import org.sonar.api.Plugin; | import org.sonar.api.Plugin; | ||||
import org.sonar.api.config.Configuration; | |||||
import org.sonar.core.platform.ExplodedPlugin; | import org.sonar.core.platform.ExplodedPlugin; | ||||
import org.sonar.core.platform.PluginClassLoader; | import org.sonar.core.platform.PluginClassLoader; | ||||
import org.sonar.core.platform.PluginInfo; | import org.sonar.core.platform.PluginInfo; | ||||
import static org.assertj.core.api.Assertions.assertThat; | import static org.assertj.core.api.Assertions.assertThat; | ||||
import static org.junit.Assert.fail; | import static org.junit.Assert.fail; | ||||
import static org.mockito.ArgumentMatchers.any; | |||||
import static org.mockito.ArgumentMatchers.anyCollection; | import static org.mockito.ArgumentMatchers.anyCollection; | ||||
import static org.mockito.ArgumentMatchers.anyMap; | import static org.mockito.ArgumentMatchers.anyMap; | ||||
import static org.mockito.ArgumentMatchers.anySet; | |||||
import static org.mockito.Mockito.mock; | import static org.mockito.Mockito.mock; | ||||
import static org.mockito.Mockito.never; | |||||
import static org.mockito.Mockito.verify; | import static org.mockito.Mockito.verify; | ||||
import static org.mockito.Mockito.when; | import static org.mockito.Mockito.when; | ||||
PluginInstaller installer = mock(PluginInstaller.class); | PluginInstaller installer = mock(PluginInstaller.class); | ||||
PluginClassLoader loader = mock(PluginClassLoader.class); | PluginClassLoader loader = mock(PluginClassLoader.class); | ||||
PluginJarExploder exploder = new FakePluginJarExploder(); | PluginJarExploder exploder = new FakePluginJarExploder(); | ||||
ScannerPluginRepository underTest = new ScannerPluginRepository(installer, exploder, loader); | |||||
Configuration properties = mock(Configuration.class); | |||||
ScannerPluginRepository underTest = new ScannerPluginRepository(installer, exploder, loader, properties); | |||||
@Test | @Test | ||||
public void install_and_load_plugins() { | public void install_and_load_plugins() { | ||||
PluginInfo info = new PluginInfo("java"); | |||||
ImmutableMap<String, ScannerPlugin> plugins = ImmutableMap.of("java", new ScannerPlugin("java", 1L, PluginType.EXTERNAL, info)); | |||||
Plugin instance = mock(Plugin.class); | |||||
when(loader.load(anyMap())).thenReturn(ImmutableMap.of("java", instance)); | |||||
when(installer.installRemotes()).thenReturn(plugins); | |||||
PluginInfo squidInfo = new PluginInfo("squid"); | |||||
PluginInfo javaInfo = new PluginInfo("java"); | |||||
Map<String, ScannerPlugin> globalPlugins = Map.of("squid", new ScannerPlugin("squid", 1L, PluginType.BUNDLED, squidInfo)); | |||||
Map<String, ScannerPlugin> languagePlugins = Map.of("java", new ScannerPlugin("java", 1L, PluginType.EXTERNAL, javaInfo)); | |||||
Plugin squidInstance = mock(Plugin.class); | |||||
Plugin javaInstance = mock(Plugin.class); | |||||
when(loader.load(anyMap())) | |||||
.thenReturn(ImmutableMap.of("squid", squidInstance)) | |||||
.thenReturn(ImmutableMap.of("java", javaInstance)); | |||||
when(installer.installRequiredPlugins()).thenReturn(globalPlugins); | |||||
when(installer.installPluginsForLanguages(anySet())).thenReturn(languagePlugins); | |||||
underTest.start(); | underTest.start(); | ||||
assertThat(underTest.getPluginInfos()).containsOnly(info); | |||||
assertThat(underTest.getPluginsByKey()).isEqualTo(plugins); | |||||
assertThat(underTest.getPluginInfo("java")).isSameAs(info); | |||||
assertThat(underTest.getPluginInstance("java")).isSameAs(instance); | |||||
assertThat(underTest.getPluginInstances()).containsOnly(instance); | |||||
assertThat(underTest.getBundledPluginsInfos()).isEmpty(); | |||||
assertThat(underTest.getExternalPluginsInfos()).isEqualTo(underTest.getPluginInfos()); | |||||
assertThat(underTest.getPluginInfos()).containsOnly(squidInfo); | |||||
assertThat(underTest.getPluginsByKey()).isEqualTo(globalPlugins); | |||||
assertThat(underTest.getPluginInfo("squid")).isSameAs(squidInfo); | |||||
assertThat(underTest.getPluginInstance("squid")).isSameAs(squidInstance); | |||||
Collection<PluginInfo> result = underTest.installPluginsForLanguages(new HashSet<>(List.of("java"))); | |||||
assertThat(result).containsOnly(javaInfo); | |||||
assertThat(underTest.getPluginInfos()).containsExactlyInAnyOrder(squidInfo, javaInfo); | |||||
assertThat(underTest.getExternalPluginsInfos()).containsExactlyInAnyOrder(javaInfo); | |||||
assertThat(underTest.getPluginsByKey().values()).containsExactlyInAnyOrder(globalPlugins.get("squid"), languagePlugins.get("java")); | |||||
underTest.stop(); | underTest.stop(); | ||||
verify(loader).unload(anyCollection()); | verify(loader).unload(anyCollection()); | ||||
} | } | ||||
@Test | |||||
public void should_install_all_plugins_when_loadall_flag_is_set() { | |||||
when(properties.getBoolean("sonar.plugins.loadAll")).thenReturn(Optional.of(true)); | |||||
underTest.start(); | |||||
verify(installer).installAllPlugins(); | |||||
verify(installer, never()).installRequiredPlugins(); | |||||
Collection<PluginInfo> result = underTest.installPluginsForLanguages(Set.of("java")); | |||||
assertThat(result).isEmpty(); | |||||
verify(installer, never()).installPluginsForLanguages(any()); | |||||
} | |||||
@Test | @Test | ||||
public void fail_if_requesting_missing_plugin() { | public void fail_if_requesting_missing_plugin() { | ||||
underTest.start(); | underTest.start(); |
*/ | */ | ||||
package org.sonar.scanner.repository.language; | package org.sonar.scanner.repository.language; | ||||
import java.io.InputStreamReader; | |||||
import java.io.StringReader; | |||||
import org.junit.Before; | |||||
import org.junit.Rule; | |||||
import org.junit.Test; | import org.junit.Test; | ||||
import org.sonar.api.resources.Language; | |||||
import org.slf4j.event.Level; | |||||
import org.sonar.api.config.Configuration; | |||||
import org.sonar.api.resources.Languages; | import org.sonar.api.resources.Languages; | ||||
import org.sonar.api.testfixtures.log.LogTester; | |||||
import org.sonar.scanner.WsTestUtil; | |||||
import org.sonar.scanner.bootstrap.DefaultScannerWsClient; | |||||
import static org.assertj.core.api.Assertions.assertThat; | import static org.assertj.core.api.Assertions.assertThat; | ||||
import static org.assertj.core.api.Assertions.tuple; | |||||
import static org.assertj.core.api.AssertionsForClassTypes.catchThrowableOfType; | |||||
import static org.mockito.Mockito.mock; | import static org.mockito.Mockito.mock; | ||||
import static org.mockito.Mockito.when; | import static org.mockito.Mockito.when; | ||||
public class DefaultLanguagesRepositoryTest { | public class DefaultLanguagesRepositoryTest { | ||||
@Rule | |||||
public LogTester logTester = new LogTester(); | |||||
private final DefaultScannerWsClient wsClient = mock(DefaultScannerWsClient.class); | |||||
private final Configuration properties = mock(Configuration.class); | |||||
private final Languages languages = mock(Languages.class); | private final Languages languages = mock(Languages.class); | ||||
private final DefaultLanguagesRepository underTest = new DefaultLanguagesRepository(languages); | |||||
private final DefaultLanguagesRepository underTest = new DefaultLanguagesRepository(wsClient, properties); | |||||
private static final String[] JAVA_SUFFIXES = new String[] { ".java", ".jav" }; | |||||
private static final String[] XOO_SUFFIXES = new String[] { ".xoo" }; | |||||
private static final String[] XOO_PATTERNS = new String[] { "Xoofile" }; | |||||
private static final String[] PYTHON_SUFFIXES = new String[] { ".py" }; | |||||
@Before | |||||
public void setup() { | |||||
logTester.setLevel(Level.DEBUG); | |||||
} | |||||
@Test | @Test | ||||
public void returns_all_languages() { | |||||
when(languages.all()).thenReturn(new Language[] {new TestLanguage("k1", true), new TestLanguage("k2", false)}); | |||||
assertThat(underTest.all()) | |||||
.extracting("key", "name", "fileSuffixes", "publishAllFiles") | |||||
.containsOnly( | |||||
tuple("k1", "name k1", new String[] {"k1"}, true), | |||||
tuple("k2", "name k2", new String[] {"k2"}, false) | |||||
); | |||||
public void should_load_languages_from_ws() { | |||||
WsTestUtil.mockReader(wsClient, "/api/languages/list", | |||||
new InputStreamReader(getClass().getResourceAsStream("DefaultLanguageRepositoryTest/languages-ws.json"))); | |||||
when(properties.getStringArray("sonar.java.file.suffixes")).thenReturn(JAVA_SUFFIXES); | |||||
when(properties.getStringArray("sonar.xoo.file.suffixes")).thenReturn(XOO_SUFFIXES); | |||||
when(properties.getStringArray("sonar.python.file.suffixes")).thenReturn(PYTHON_SUFFIXES); | |||||
underTest.start(); | |||||
underTest.stop(); | |||||
assertThat(underTest.all()).hasSize(3); | |||||
assertThat(underTest.get("java")).isNotNull(); | |||||
assertThat(underTest.get("java").fileSuffixes()).containsExactlyInAnyOrder(JAVA_SUFFIXES); | |||||
assertThat(underTest.get("java").isPublishAllFiles()).isTrue(); | |||||
assertThat(underTest.get("xoo")).isNotNull(); | |||||
assertThat(underTest.get("xoo").fileSuffixes()).containsExactlyInAnyOrder(XOO_SUFFIXES); | |||||
assertThat(underTest.get("xoo").isPublishAllFiles()).isTrue(); | |||||
assertThat(underTest.get("python")).isNotNull(); | |||||
assertThat(underTest.get("python").fileSuffixes()).containsExactlyInAnyOrder(PYTHON_SUFFIXES); | |||||
assertThat(underTest.get("python").isPublishAllFiles()).isTrue(); | |||||
} | |||||
@Test | |||||
public void should_throw_error_on_invalid_ws_response() { | |||||
WsTestUtil.mockReader(wsClient, "api/languages/list", new StringReader("not json")); | |||||
IllegalStateException e = catchThrowableOfType(underTest::start, IllegalStateException.class); | |||||
assertThat(e).hasMessage("Fail to parse response of /api/languages/list"); | |||||
} | |||||
@Test | |||||
public void should_return_null_when_language_not_found() { | |||||
WsTestUtil.mockReader(wsClient, "/api/languages/list", | |||||
new InputStreamReader(getClass().getResourceAsStream("DefaultLanguageRepositoryTest/languages-ws.json"))); | |||||
when(properties.getStringArray("sonar.java.file.suffixes")).thenReturn(JAVA_SUFFIXES); | |||||
when(properties.getStringArray("sonar.xoo.file.suffixes")).thenReturn(XOO_SUFFIXES); | |||||
when(properties.getStringArray("sonar.python.file.suffixes")).thenReturn(PYTHON_SUFFIXES); | |||||
assertThat(underTest.get("k1")).isNull(); | |||||
} | } | ||||
@Test | @Test | ||||
public void publishAllFiles_by_default() { | public void publishAllFiles_by_default() { | ||||
when(languages.all()).thenReturn(new Language[] {new TestLanguage2("k1"), new TestLanguage2("k2")}); | |||||
assertThat(underTest.all()) | |||||
.extracting("key", "name", "fileSuffixes", "publishAllFiles") | |||||
.containsOnly( | |||||
tuple("k1", "name k1", new String[] {"k1"}, true), | |||||
tuple("k2", "name k2", new String[] {"k2"}, true) | |||||
); | |||||
WsTestUtil.mockReader(wsClient, "/api/languages/list", | |||||
new InputStreamReader(getClass().getResourceAsStream("DefaultLanguageRepositoryTest/languages-ws.json"))); | |||||
underTest.start(); | |||||
underTest.stop(); | |||||
assertThat(underTest.get("java").isPublishAllFiles()).isTrue(); | |||||
assertThat(underTest.get("xoo").isPublishAllFiles()).isTrue(); | |||||
assertThat(underTest.get("python").isPublishAllFiles()).isTrue(); | |||||
} | } | ||||
@Test | @Test | ||||
public void get_find_language_by_key() { | public void get_find_language_by_key() { | ||||
when(languages.get("k1")).thenReturn(new TestLanguage2("k1")); | |||||
assertThat(underTest.get("k1")) | |||||
.extracting("key", "name", "fileSuffixes", "publishAllFiles") | |||||
.containsOnly("k1", "name k1", new String[] {"k1"}, true); | |||||
} | |||||
WsTestUtil.mockReader(wsClient, "/api/languages/list", | |||||
new InputStreamReader(getClass().getResourceAsStream("DefaultLanguageRepositoryTest/languages-ws.json"))); | |||||
when(properties.getStringArray("sonar.java.file.suffixes")).thenReturn(JAVA_SUFFIXES); | |||||
private static class TestLanguage implements Language { | |||||
private final String key; | |||||
private final boolean publishAllFiles; | |||||
public TestLanguage(String key, boolean publishAllFiles) { | |||||
this.key = key; | |||||
this.publishAllFiles = publishAllFiles; | |||||
} | |||||
@Override | |||||
public String getKey() { | |||||
return key; | |||||
} | |||||
@Override | |||||
public String getName() { | |||||
return "name " + key; | |||||
} | |||||
@Override | |||||
public String[] getFileSuffixes() { | |||||
return new String[] {key}; | |||||
} | |||||
@Override | |||||
public boolean publishAllFiles() { | |||||
return publishAllFiles; | |||||
} | |||||
underTest.start(); | |||||
underTest.stop(); | |||||
assertThat(underTest.get("java")) | |||||
.extracting("key", "name", "fileSuffixes", "publishAllFiles") | |||||
.containsOnly("java", "Java", JAVA_SUFFIXES, true); | |||||
} | } | ||||
private static class TestLanguage2 implements Language { | |||||
private final String key; | |||||
public TestLanguage2(String key) { | |||||
this.key = key; | |||||
} | |||||
@Test | |||||
public void should_log_if_language_has_no_suffixes_or_patterns() { | |||||
WsTestUtil.mockReader(wsClient, "/api/languages/list", | |||||
new InputStreamReader(getClass().getResourceAsStream("DefaultLanguageRepositoryTest/languages-ws.json"))); | |||||
@Override | |||||
public String getKey() { | |||||
return key; | |||||
} | |||||
when(properties.getStringArray("sonar.java.file.suffixes")).thenReturn(JAVA_SUFFIXES); | |||||
when(properties.getStringArray("sonar.xoo.file.patterns")).thenReturn(XOO_PATTERNS); | |||||
@Override | |||||
public String getName() { | |||||
return "name " + key; | |||||
} | |||||
underTest.start(); | |||||
@Override | |||||
public String[] getFileSuffixes() { | |||||
return new String[] {key}; | |||||
} | |||||
assertThat(logTester.logs(Level.DEBUG)).contains("Language 'Python' cannot be detected as it has neither suffixes nor patterns."); | |||||
} | } | ||||
} | } |
entry("sonar.issue.exclusions.multicriteria.2.rulepattern", "*:S456")); | entry("sonar.issue.exclusions.multicriteria.2.rulepattern", "*:S456")); | ||||
} | } | ||||
@Test | |||||
public void should_always_load_language_detection_properties() { | |||||
assertThat(AbstractSettingsLoader.toMap(List.of( | |||||
Setting.newBuilder() | |||||
.setInherited(false) | |||||
.setKey("sonar.xoo.file.suffixes") | |||||
.setValues(Values.newBuilder().addValues(".xoo")).build(), | |||||
Setting.newBuilder() | |||||
.setInherited(false) | |||||
.setKey("sonar.xoo.file.patterns") | |||||
.setValues(Values.newBuilder().addValues("Xoofile")).build() | |||||
))).containsExactly( | |||||
entry("sonar.xoo.file.suffixes", ".xoo"), | |||||
entry("sonar.xoo.file.patterns", "Xoofile") | |||||
); | |||||
} | |||||
@Test | |||||
public void should_not_load_inherited_properties() { | |||||
assertThat(AbstractSettingsLoader.toMap(List.of( | |||||
Setting.newBuilder() | |||||
.setInherited(true) | |||||
.setKey("sonar.inherited.property") | |||||
.setValues(Values.newBuilder().addValues("foo")).build() | |||||
))).isEmpty(); | |||||
} | |||||
} | } |
import org.sonar.api.resources.Language; | import org.sonar.api.resources.Language; | ||||
import org.sonar.api.resources.Languages; | import org.sonar.api.resources.Languages; | ||||
import org.sonar.api.utils.MessageException; | import org.sonar.api.utils.MessageException; | ||||
import org.sonar.scanner.repository.language.DefaultLanguagesRepository; | |||||
import org.sonar.scanner.mediumtest.FakeLanguagesRepository; | |||||
import org.sonar.scanner.repository.language.LanguagesRepository; | import org.sonar.scanner.repository.language.LanguagesRepository; | ||||
import static org.assertj.core.api.Assertions.assertThat; | import static org.assertj.core.api.Assertions.assertThat; | ||||
@Test | @Test | ||||
public void detectLanguageKey_shouldDetectByFileExtension() { | public void detectLanguageKey_shouldDetectByFileExtension() { | ||||
LanguagesRepository languages = new DefaultLanguagesRepository(new Languages(new MockLanguage("java", "java", "jav"), new MockLanguage("cobol", "cbl", "cob"))); | |||||
LanguagesRepository languages = new FakeLanguagesRepository(new Languages(new MockLanguage("java", "java", "jav"), new MockLanguage("cobol", "cbl", "cob"))); | |||||
LanguageDetection detection = new LanguageDetection(settings.asConfig(), languages); | LanguageDetection detection = new LanguageDetection(settings.asConfig(), languages); | ||||
assertThat(detectLanguageKey(detection, "Foo.java")).isEqualTo("java"); | assertThat(detectLanguageKey(detection, "Foo.java")).isEqualTo("java"); | ||||
@Test | @Test | ||||
@UseDataProvider("filenamePatterns") | @UseDataProvider("filenamePatterns") | ||||
public void detectLanguageKey_shouldDetectByFileNamePattern(String fileName, String expectedLanguageKey) { | public void detectLanguageKey_shouldDetectByFileNamePattern(String fileName, String expectedLanguageKey) { | ||||
LanguagesRepository languages = new DefaultLanguagesRepository(new Languages( | |||||
LanguagesRepository languages = new FakeLanguagesRepository(new Languages( | |||||
new MockLanguage("docker", new String[0], new String[] {"*.dockerfile", "*.Dockerfile", "Dockerfile", "Dockerfile.*"}), | new MockLanguage("docker", new String[0], new String[] {"*.dockerfile", "*.Dockerfile", "Dockerfile", "Dockerfile.*"}), | ||||
new MockLanguage("terraform", new String[] {"tf"}, new String[] {".tf"}), | new MockLanguage("terraform", new String[] {"tf"}, new String[] {".tf"}), | ||||
new MockLanguage("java", new String[0], new String[] {"**/*Test.java"}))); | new MockLanguage("java", new String[0], new String[] {"**/*Test.java"}))); | ||||
@Test | @Test | ||||
public void detectLanguageKey_shouldNotFailIfNoLanguage() { | public void detectLanguageKey_shouldNotFailIfNoLanguage() { | ||||
LanguageDetection detection = spy(new LanguageDetection(settings.asConfig(), new DefaultLanguagesRepository(new Languages()))); | |||||
LanguageDetection detection = spy(new LanguageDetection(settings.asConfig(), new FakeLanguagesRepository(new Languages()))); | |||||
assertThat(detectLanguageKey(detection, "Foo.java")).isNull(); | assertThat(detectLanguageKey(detection, "Foo.java")).isNull(); | ||||
} | } | ||||
@Test | @Test | ||||
public void detectLanguageKey_shouldAllowPluginsToDeclareFileExtensionTwiceForCaseSensitivity() { | public void detectLanguageKey_shouldAllowPluginsToDeclareFileExtensionTwiceForCaseSensitivity() { | ||||
LanguagesRepository languages = new DefaultLanguagesRepository(new Languages(new MockLanguage("abap", "abap", "ABAP"))); | |||||
LanguagesRepository languages = new FakeLanguagesRepository(new Languages(new MockLanguage("abap", "abap", "ABAP"))); | |||||
LanguageDetection detection = new LanguageDetection(settings.asConfig(), languages); | LanguageDetection detection = new LanguageDetection(settings.asConfig(), languages); | ||||
assertThat(detectLanguageKey(detection, "abc.abap")).isEqualTo("abap"); | assertThat(detectLanguageKey(detection, "abc.abap")).isEqualTo("abap"); | ||||
@Test | @Test | ||||
public void detectLanguageKey_shouldFailIfConflictingLanguageSuffix() { | public void detectLanguageKey_shouldFailIfConflictingLanguageSuffix() { | ||||
LanguagesRepository languages = new DefaultLanguagesRepository(new Languages(new MockLanguage("xml", "xhtml"), new MockLanguage("web", "xhtml"))); | |||||
LanguagesRepository languages = new FakeLanguagesRepository(new Languages(new MockLanguage("xml", "xhtml"), new MockLanguage("web", "xhtml"))); | |||||
LanguageDetection detection = new LanguageDetection(settings.asConfig(), languages); | LanguageDetection detection = new LanguageDetection(settings.asConfig(), languages); | ||||
assertThatThrownBy(() -> detectLanguageKey(detection, "abc.xhtml")) | assertThatThrownBy(() -> detectLanguageKey(detection, "abc.xhtml")) | ||||
.isInstanceOf(MessageException.class) | .isInstanceOf(MessageException.class) | ||||
@Test | @Test | ||||
public void detectLanguageKey_shouldSolveConflictUsingFilePattern() { | public void detectLanguageKey_shouldSolveConflictUsingFilePattern() { | ||||
LanguagesRepository languages = new DefaultLanguagesRepository(new Languages(new MockLanguage("xml", "xhtml"), new MockLanguage("web", "xhtml"))); | |||||
LanguagesRepository languages = new FakeLanguagesRepository(new Languages(new MockLanguage("xml", "xhtml"), new MockLanguage("web", "xhtml"))); | |||||
settings.setProperty("sonar.lang.patterns.xml", "xml/**"); | settings.setProperty("sonar.lang.patterns.xml", "xml/**"); | ||||
settings.setProperty("sonar.lang.patterns.web", "web/**"); | settings.setProperty("sonar.lang.patterns.web", "web/**"); | ||||
@Test | @Test | ||||
public void detectLanguageKey_shouldFailIfConflictingFilePattern() { | public void detectLanguageKey_shouldFailIfConflictingFilePattern() { | ||||
LanguagesRepository languages = new DefaultLanguagesRepository(new Languages(new MockLanguage("abap", "abap"), new MockLanguage("cobol", "cobol"))); | |||||
LanguagesRepository languages = new FakeLanguagesRepository(new Languages(new MockLanguage("abap", "abap"), new MockLanguage("cobol", "cobol"))); | |||||
settings.setProperty("sonar.lang.patterns.abap", "*.abap,*.txt"); | settings.setProperty("sonar.lang.patterns.abap", "*.abap,*.txt"); | ||||
settings.setProperty("sonar.lang.patterns.cobol", "*.cobol,*.txt"); | settings.setProperty("sonar.lang.patterns.cobol", "*.cobol,*.txt"); | ||||
"key": "scmgit", | "key": "scmgit", | ||||
"hash": "abc", | "hash": "abc", | ||||
"type": "BUNDLED", | "type": "BUNDLED", | ||||
"updatedAt": 100 | |||||
"updatedAt": 100, | |||||
"requiredForLanguages": [] | |||||
}, | }, | ||||
{ | { | ||||
"key": "java", | "key": "java", | ||||
"hash": "def", | "hash": "def", | ||||
"type": "EXTERNAL", | "type": "EXTERNAL", | ||||
"updatedAt": 200 | |||||
"updatedAt": 200, | |||||
"requiredForLanguages": [ | |||||
"java" | |||||
] | |||||
} | } | ||||
] | ] | ||||
} | } |
{ | |||||
"languages": [ | |||||
{ | |||||
"key": "java", | |||||
"name": "Java" | |||||
}, | |||||
{ | |||||
"key" : "xoo", | |||||
"name": "Xoo" | |||||
}, | |||||
{ | |||||
"key": "python", | |||||
"name": "Python" | |||||
} | |||||
] | |||||
} |