@@ -85,15 +85,6 @@ public class SpringComponentContainer implements StartableContainer { | |||
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. | |||
* The strategy is: |
@@ -106,7 +106,7 @@ public class ScannerMediumTester extends ExternalResource { | |||
private final FakeActiveRulesLoader activeRules = new FakeActiveRulesLoader(); | |||
private final FakeSonarRuntime sonarRuntime = new FakeSonarRuntime(); | |||
private final CeTaskReportDataHolder reportMetadataHolder = new CeTaskReportDataHolderExt(); | |||
private final FakeLanguagesRepository languagesRepository = new FakeLanguagesRepository(); | |||
private final FakeLanguagesLoader languagesLoader = new FakeLanguagesLoader(); | |||
private LogOutput logOutput = null; | |||
private static void createWorkingDirs() throws IOException { | |||
@@ -286,11 +286,11 @@ public class ScannerMediumTester extends ExternalResource { | |||
} | |||
public void addLanguage(String key, String name, String... suffixes) { | |||
languagesRepository.addLanguage(key, name, suffixes, new String[0]); | |||
languagesLoader.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); | |||
languagesLoader.addLanguage(key, name, suffixes, new String[0], publishAllFiles); | |||
} | |||
public static class AnalysisBuilder { | |||
@@ -324,8 +324,8 @@ public class ScannerMediumTester extends ExternalResource { | |||
tester.analysisCacheLoader, | |||
tester.sonarRuntime, | |||
tester.reportMetadataHolder, | |||
result, | |||
tester.languagesRepository); | |||
tester.languagesLoader, | |||
result); | |||
if (tester.logOutput != null) { | |||
builder.setLogOutput(tester.logOutput); | |||
} else { |
@@ -86,8 +86,8 @@ 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.DefaultLanguagesLoader; | |||
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; | |||
@@ -304,10 +304,9 @@ public class SpringScannerContainer extends SpringComponentContainer { | |||
add(DefaultProjectSettingsLoader.class, | |||
DefaultActiveRulesLoader.class, | |||
DefaultQualityProfileLoader.class, | |||
DefaultProjectRepositoriesLoader.class); | |||
addIfMissing(DefaultLanguagesRepository.class, LanguagesRepository.class); | |||
DefaultProjectRepositoriesLoader.class, | |||
DefaultLanguagesLoader.class, | |||
DefaultLanguagesRepository.class); | |||
} | |||
static ExtensionMatcher getScannerProjectExtensionsFilter() { |
@@ -1,6 +1,6 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* Copyright (C) 2009-2024 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
@@ -19,43 +19,31 @@ | |||
*/ | |||
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.LanguagesLoader; | |||
import org.sonar.scanner.repository.language.SupportedLanguageDto; | |||
@Priority(1) | |||
public class FakeLanguagesRepository implements LanguagesRepository { | |||
public class FakeLanguagesLoader implements LanguagesLoader { | |||
private final Map<String, Language> languageMap = new HashMap<>(); | |||
public FakeLanguagesRepository() { | |||
public FakeLanguagesLoader() { | |||
languageMap.put("xoo", new Language(new FakeLanguage("xoo", "xoo", new String[] { ".xoo" }, new String[0], true))); | |||
} | |||
public FakeLanguagesRepository(Languages languages) { | |||
public FakeLanguagesLoader(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 Map<String, Language> load() { | |||
return languageMap; | |||
} | |||
public void addLanguage(String key, String name, String[] suffixes, String[] filenamePatterns) { |
@@ -0,0 +1,96 @@ | |||
/* | |||
* 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; | |||
import com.google.gson.Gson; | |||
import java.io.Reader; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.function.Function; | |||
import java.util.stream.Collectors; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.api.config.Configuration; | |||
import org.sonar.scanner.bootstrap.DefaultScannerWsClient; | |||
import org.sonarqube.ws.client.GetRequest; | |||
public class DefaultLanguagesLoader implements LanguagesLoader { | |||
private static final Logger LOG = LoggerFactory.getLogger(DefaultLanguagesLoader.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 DefaultScannerWsClient wsClient; | |||
private final Configuration properties; | |||
public DefaultLanguagesLoader(DefaultScannerWsClient wsClient, Configuration properties) { | |||
this.wsClient = wsClient; | |||
this.properties = properties; | |||
} | |||
@Override | |||
public Map<String, Language> load() { | |||
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); | |||
} | |||
return response.languages.stream() | |||
.map(this::populateFileSuffixesAndPatterns) | |||
.collect(Collectors.toMap(Language::key, Function.identity())); | |||
} | |||
private Language populateFileSuffixesAndPatterns(SupportedLanguageDto lang) { | |||
lang.setFileSuffixes(getFileSuffixes(lang.getKey())); | |||
lang.setFilenamePatterns(getFilenamePatterns(lang.getKey())); | |||
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) { | |||
return getPropertyForLanguage("sonar.%s.file.suffixes", languageKey); | |||
} | |||
private String[] getFilenamePatterns(String languageKey) { | |||
return getPropertyForLanguage("sonar.%s.file.patterns", languageKey); | |||
} | |||
private String[] getPropertyForLanguage(String propertyPattern, String languageKey) { | |||
String propName = String.format(propertyPattern, PROPERTY_FRAGMENT_MAP.getOrDefault(languageKey, languageKey)); | |||
return properties.getStringArray(propName); | |||
} | |||
private static class LanguagesWSResponse { | |||
List<SupportedLanguageDto> languages; | |||
} | |||
} |
@@ -19,23 +19,13 @@ | |||
*/ | |||
package org.sonar.scanner.repository.language; | |||
import com.google.gson.Gson; | |||
import java.io.Reader; | |||
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 org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import javax.annotation.concurrent.Immutable; | |||
import org.sonar.api.Startable; | |||
import org.sonar.api.config.Configuration; | |||
import org.sonar.api.resources.Languages; | |||
import org.sonar.scanner.bootstrap.DefaultScannerWsClient; | |||
import org.sonarqube.ws.client.GetRequest; | |||
/** | |||
* Languages repository using {@link Languages} | |||
@@ -43,59 +33,17 @@ import org.sonarqube.ws.client.GetRequest; | |||
*/ | |||
@Immutable | |||
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 Map<String, Language> languages = new HashMap<>(); | |||
private final DefaultScannerWsClient wsClient; | |||
private final Configuration properties; | |||
private final LanguagesLoader languagesLoader; | |||
public DefaultLanguagesRepository(DefaultScannerWsClient wsClient, Configuration properties) { | |||
this.wsClient = wsClient; | |||
this.properties = properties; | |||
public DefaultLanguagesRepository(LanguagesLoader languagesLoader) { | |||
this.languagesLoader = languagesLoader; | |||
} | |||
@Override | |||
public void start() { | |||
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) { | |||
lang.setFileSuffixes(getFileSuffixes(lang.getKey())); | |||
lang.setFilenamePatterns(getFilenamePatterns(lang.getKey())); | |||
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) { | |||
return getPropertyForLanguage("sonar.%s.file.suffixes", languageKey); | |||
} | |||
private String[] getFilenamePatterns(String languageKey) { | |||
return getPropertyForLanguage("sonar.%s.file.patterns", languageKey); | |||
} | |||
private String[] getPropertyForLanguage(String propertyPattern, String languageKey) { | |||
String propName = String.format(propertyPattern, PROPERTY_FRAGMENT_MAP.getOrDefault(languageKey, languageKey)); | |||
return properties.getStringArray(propName); | |||
languages.putAll(languagesLoader.load()); | |||
} | |||
/** | |||
@@ -120,8 +68,5 @@ public class DefaultLanguagesRepository implements LanguagesRepository, Startabl | |||
// nothing to do | |||
} | |||
private static class LanguagesWSResponse { | |||
List<SupportedLanguageDto> languages; | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
/* | |||
* 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; | |||
import java.util.Map; | |||
public interface LanguagesLoader { | |||
Map<String, Language> load(); | |||
} |
@@ -26,7 +26,6 @@ import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.slf4j.event.Level; | |||
import org.sonar.api.config.Configuration; | |||
import org.sonar.api.resources.Languages; | |||
import org.sonar.api.testfixtures.log.LogTester; | |||
import org.sonar.scanner.WsTestUtil; | |||
import org.sonar.scanner.bootstrap.DefaultScannerWsClient; | |||
@@ -43,8 +42,10 @@ public class DefaultLanguagesRepositoryTest { | |||
private final DefaultScannerWsClient wsClient = mock(DefaultScannerWsClient.class); | |||
private final Configuration properties = mock(Configuration.class); | |||
private final Languages languages = mock(Languages.class); | |||
private final DefaultLanguagesRepository underTest = new DefaultLanguagesRepository(wsClient, properties); | |||
private final LanguagesLoader languagesLoader = new DefaultLanguagesLoader(wsClient, properties); | |||
private final DefaultLanguagesRepository underTest = new DefaultLanguagesRepository(languagesLoader); | |||
private static final String[] JAVA_SUFFIXES = new String[] { ".java", ".jav" }; | |||
private static final String[] XOO_SUFFIXES = new String[] { ".xoo" }; | |||
@@ -66,7 +67,6 @@ public class DefaultLanguagesRepositoryTest { | |||
when(properties.getStringArray("sonar.python.file.suffixes")).thenReturn(PYTHON_SUFFIXES); | |||
underTest.start(); | |||
underTest.stop(); | |||
assertThat(underTest.all()).hasSize(3); | |||
assertThat(underTest.get("java")).isNotNull(); | |||
@@ -107,7 +107,6 @@ public class DefaultLanguagesRepositoryTest { | |||
new InputStreamReader(getClass().getResourceAsStream("DefaultLanguageRepositoryTest/languages-ws.json"))); | |||
underTest.start(); | |||
underTest.stop(); | |||
assertThat(underTest.get("java").isPublishAllFiles()).isTrue(); | |||
assertThat(underTest.get("xoo").isPublishAllFiles()).isTrue(); | |||
@@ -122,7 +121,6 @@ public class DefaultLanguagesRepositoryTest { | |||
when(properties.getStringArray("sonar.java.file.suffixes")).thenReturn(JAVA_SUFFIXES); | |||
underTest.start(); | |||
underTest.stop(); | |||
assertThat(underTest.get("java")) | |||
.extracting("key", "name", "fileSuffixes", "publishAllFiles") |
@@ -35,8 +35,8 @@ import org.sonar.api.config.internal.MapSettings; | |||
import org.sonar.api.resources.Language; | |||
import org.sonar.api.resources.Languages; | |||
import org.sonar.api.utils.MessageException; | |||
import org.sonar.scanner.mediumtest.FakeLanguagesRepository; | |||
import org.sonar.scanner.repository.language.LanguagesRepository; | |||
import org.sonar.scanner.mediumtest.FakeLanguagesLoader; | |||
import org.sonar.scanner.repository.language.DefaultLanguagesRepository; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
@@ -77,7 +77,8 @@ public class LanguageDetectionTest { | |||
@Test | |||
public void detectLanguageKey_shouldDetectByFileExtension() { | |||
LanguagesRepository languages = new FakeLanguagesRepository(new Languages(new MockLanguage("java", "java", "jav"), new MockLanguage("cobol", "cbl", "cob"))); | |||
DefaultLanguagesRepository languages = new DefaultLanguagesRepository(new FakeLanguagesLoader(new Languages(new MockLanguage("java", "java", "jav"), new MockLanguage("cobol", "cbl", "cob")))); | |||
languages.start(); | |||
LanguageDetection detection = new LanguageDetection(settings.asConfig(), languages, new HashMap<>()); | |||
assertThat(detectLanguageKey(detection, "Foo.java")).isEqualTo("java"); | |||
@@ -96,10 +97,11 @@ public class LanguageDetectionTest { | |||
@Test | |||
@UseDataProvider("filenamePatterns") | |||
public void detectLanguageKey_shouldDetectByFileNamePattern(String fileName, String expectedLanguageKey) { | |||
LanguagesRepository languages = new FakeLanguagesRepository(new Languages( | |||
DefaultLanguagesRepository languages = new DefaultLanguagesRepository(new FakeLanguagesLoader(new Languages( | |||
new MockLanguage("docker", new String[0], new String[] {"*.dockerfile", "*.Dockerfile", "Dockerfile", "Dockerfile.*"}), | |||
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"})))); | |||
languages.start(); | |||
LanguageDetection detection = new LanguageDetection(settings.asConfig(), languages, new HashMap<>()); | |||
assertThat(detectLanguageKey(detection, fileName)).isEqualTo(expectedLanguageKey); | |||
} | |||
@@ -123,13 +125,14 @@ public class LanguageDetectionTest { | |||
@Test | |||
public void detectLanguageKey_shouldNotFailIfNoLanguage() { | |||
LanguageDetection detection = spy(new LanguageDetection(settings.asConfig(), new FakeLanguagesRepository(new Languages()), new HashMap<>())); | |||
LanguageDetection detection = spy(new LanguageDetection(settings.asConfig(), new DefaultLanguagesRepository(new FakeLanguagesLoader(new Languages())), new HashMap<>())); | |||
assertThat(detectLanguageKey(detection, "Foo.java")).isNull(); | |||
} | |||
@Test | |||
public void detectLanguageKey_shouldAllowPluginsToDeclareFileExtensionTwiceForCaseSensitivity() { | |||
LanguagesRepository languages = new FakeLanguagesRepository(new Languages(new MockLanguage("abap", "abap", "ABAP"))); | |||
DefaultLanguagesRepository languages = new DefaultLanguagesRepository(new FakeLanguagesLoader(new Languages(new MockLanguage("abap", "abap", "ABAP")))); | |||
languages.start(); | |||
LanguageDetection detection = new LanguageDetection(settings.asConfig(), languages, new HashMap<>()); | |||
assertThat(detectLanguageKey(detection, "abc.abap")).isEqualTo("abap"); | |||
@@ -137,7 +140,8 @@ public class LanguageDetectionTest { | |||
@Test | |||
public void detectLanguageKey_shouldFailIfConflictingLanguageSuffix() { | |||
LanguagesRepository languages = new FakeLanguagesRepository(new Languages(new MockLanguage("xml", "xhtml"), new MockLanguage("web", "xhtml"))); | |||
DefaultLanguagesRepository languages = new DefaultLanguagesRepository(new FakeLanguagesLoader(new Languages(new MockLanguage("xml", "xhtml"), new MockLanguage("web", "xhtml")))); | |||
languages.start(); | |||
LanguageDetection detection = new LanguageDetection(settings.asConfig(), languages, new HashMap<>()); | |||
assertThatThrownBy(() -> detectLanguageKey(detection, "abc.xhtml")) | |||
.isInstanceOf(MessageException.class) | |||
@@ -148,7 +152,8 @@ public class LanguageDetectionTest { | |||
@Test | |||
public void detectLanguageKey_shouldSolveConflictUsingFilePattern() { | |||
LanguagesRepository languages = new FakeLanguagesRepository(new Languages(new MockLanguage("xml", "xhtml"), new MockLanguage("web", "xhtml"))); | |||
DefaultLanguagesRepository languages = new DefaultLanguagesRepository(new FakeLanguagesLoader(new Languages(new MockLanguage("xml", "xhtml"), new MockLanguage("web", "xhtml")))); | |||
languages.start(); | |||
settings.setProperty("sonar.lang.patterns.xml", "xml/**"); | |||
settings.setProperty("sonar.lang.patterns.web", "web/**"); | |||
@@ -159,7 +164,8 @@ public class LanguageDetectionTest { | |||
@Test | |||
public void detectLanguageKey_shouldFailIfConflictingFilePattern() { | |||
LanguagesRepository languages = new FakeLanguagesRepository(new Languages(new MockLanguage("abap", "abap"), new MockLanguage("cobol", "cobol"))); | |||
DefaultLanguagesRepository languages = new DefaultLanguagesRepository(new FakeLanguagesLoader(new Languages(new MockLanguage("abap", "abap"), new MockLanguage("cobol", "cobol")))); | |||
languages.start(); | |||
settings.setProperty("sonar.lang.patterns.abap", "*.abap,*.txt"); | |||
settings.setProperty("sonar.lang.patterns.cobol", "*.cobol,*.txt"); | |||
@@ -177,8 +183,9 @@ public class LanguageDetectionTest { | |||
@Test | |||
public void should_cache_detected_language_by_file_path() { | |||
Map<String, org.sonar.scanner.repository.language.Language> languageCacheSpy = spy(new HashMap<>()); | |||
LanguagesRepository languages = new FakeLanguagesRepository(new Languages( | |||
new MockLanguage("java", "java", "jav"), new MockLanguage("cobol", "cbl", "cob"))); | |||
DefaultLanguagesRepository languages = new DefaultLanguagesRepository(new FakeLanguagesLoader(new Languages( | |||
new MockLanguage("java", "java", "jav"), new MockLanguage("cobol", "cbl", "cob")))); | |||
languages.start(); | |||
LanguageDetection detection = new LanguageDetection(settings.asConfig(), languages, languageCacheSpy); | |||
assertThat(detectLanguageKey(detection, "Foo.java")).isEqualTo("java"); |