All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.java.SonarComponents Maven / Gradle / Ivy

/*
 * SonarQube Java
 * Copyright (C) 2012-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 Sonar Source-Available License Version 1, as published by SonarSource SA.
 *
 * 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 Sonar Source-Available License for more details.
 *
 * You should have received a copy of the Sonar Source-Available License
 * along with this program; if not, see https://sonarsource.com/license/ssal/
 */
package org.sonar.java;

import com.sonar.sslr.api.RecognitionException;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.LongSupplier;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.SonarProduct;
import org.sonar.api.batch.ScannerSide;
import org.sonar.api.batch.bootstrap.ProjectDefinition;
import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputComponent;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.rule.ActiveRules;
import org.sonar.api.batch.rule.CheckFactory;
import org.sonar.api.batch.rule.Checks;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
import org.sonar.api.batch.sensor.symbol.NewSymbolTable;
import org.sonar.api.config.Configuration;
import org.sonar.api.measures.FileLinesContext;
import org.sonar.api.measures.FileLinesContextFactory;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.utils.Version;
import org.sonar.java.annotations.VisibleForTesting;
import org.sonar.java.caching.ContentHashCache;
import org.sonar.java.classpath.ClasspathForMain;
import org.sonar.java.classpath.ClasspathForTest;
import org.sonar.java.exceptions.ApiMismatchException;
import org.sonar.java.model.GeneratedFile;
import org.sonar.java.model.JProblem;
import org.sonar.java.model.LineUtils;
import org.sonar.java.reporting.AnalyzerMessage;
import org.sonar.java.reporting.JavaIssue;
import org.sonar.plugins.java.api.CheckRegistrar;
import org.sonar.plugins.java.api.JavaCheck;
import org.sonar.plugins.java.api.JspCodeVisitor;
import org.sonar.plugins.java.api.caching.SonarLintCache;
import org.sonarsource.api.sonarlint.SonarLintSide;
import org.sonarsource.sonarlint.plugin.api.SonarLintRuntime;

@ScannerSide
@SonarLintSide
public class SonarComponents extends CheckRegistrar.RegistrarContext {

  private static final Logger LOG = LoggerFactory.getLogger(SonarComponents.class);
  private static final int LOGGED_MAX_NUMBER_UNDEFINED_TYPES = 50;

  public static final String FAIL_ON_EXCEPTION_KEY = "sonar.internal.analysis.failFast";
  public static final String SONAR_BATCH_MODE_KEY = "sonar.java.internal.batchMode";
  public static final String SONAR_AUTOSCAN = "sonar.internal.analysis.autoscan";
  public static final String SONAR_AUTOSCAN_CHECK_FILTERING = "sonar.internal.analysis.autoscan.filtering";
  public static final String SONAR_BATCH_SIZE_KEY = "sonar.java.experimental.batchModeSizeInKB";
  public static final String SONAR_FILE_BY_FILE = "sonar.java.fileByFile";
  /**
   * Describes if an optimized analysis of unchanged by skipping some rules is enabled.
   * By default, the property is not set (null), leaving SQ/SC to decide whether to enable this behavior.
   * Setting it to true or false, forces the behavior from the analyzer independently of the server.
   */
  public static final String SONAR_CAN_SKIP_UNCHANGED_FILES_KEY = "sonar.java.skipUnchanged";

  /**
   * Describes whether input files should be parsed while ignoring unnamed split modules.
   * In practice, enabling this parameter should help developers in the Android ecosystem and those
   * relying on (transitive) dependencies that do not respect modularization as defined by the JLS.
   */
  public static final String SONAR_IGNORE_UNNAMED_MODULE_FOR_SPLIT_PACKAGE = "sonar.java.ignoreUnnamedModuleForSplitPackage";
  private static final Version SONARLINT_6_3 = Version.parse("6.3");
  private static final Version SONARQUBE_9_2 = Version.parse("9.2");
  @VisibleForTesting
  static LongSupplier maxMemoryInBytesProvider = () -> Runtime.getRuntime().maxMemory();

  private final FileLinesContextFactory fileLinesContextFactory;

  private final ClasspathForMain javaClasspath;
  private final ClasspathForTest javaTestClasspath;
  private final Map> problemsToFilePaths = new HashMap<>();

  private final CheckFactory checkFactory;
  private final ActiveRules activeRules;
  @Nullable
  private final ProjectDefinition projectDefinition;
  @Nullable
  private final SonarLintCache sonarLintCache;
  private final FileSystem fs;
  private final List mainChecks;
  private final List testChecks;
  private final List jspChecks;
  private final List> allChecks;
  private SensorContext context;
  private UnaryOperator> checkFilter = UnaryOperator.identity();
  private final Set additionalAutoScanCompatibleRuleKeys;

  private boolean alreadyLoggedSkipStatus = false;

  public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs,
    ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath,
    CheckFactory checkFactory, ActiveRules activeRules) {
    this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, null, null, null);
  }

  /**
   * Can be called in SonarLint context when custom rules are present.
   */
  public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs,
    ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory,
    ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars) {
    this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, checkRegistrars, null, null);
  }

  /**
   * Will *only* be called in SonarLint context and when custom rules are present.
   * 

* This is because {@link SonarLintCache} is only added as an extension in a SonarLint context. * See also {@code JavaPlugin#define} in the {@code sonar-java-plugin} module. *

* {@code SonarLintCache} is used only by newer custom rules, e.g. DBD. * Thus, for this constructor, we can also assume the presence of {@code CheckRegistrar} instances. */ public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs, ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory, ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars, SonarLintCache sonarLintCache) { this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, checkRegistrars, null, sonarLintCache); } /** * Will be called in SonarScanner context when no custom rules are present. * May be called in some SonarLint contexts, but not others, since ProjectDefinition might not be available. */ public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs, ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory, ActiveRules activeRules, @Nullable ProjectDefinition projectDefinition) { this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, null, projectDefinition, null); } /** * May be called in some SonarLint contexts, but not others, since ProjectDefinition might not be available. */ public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs, ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory, ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars, @Nullable ProjectDefinition projectDefinition) { this( fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, checkRegistrars, projectDefinition, null ); } /** * All other constructors delegate to this one. *

* It will also be called directly when constructing a SonarComponents instance for injection if all parameters are available. * This is for example the case for SonarLint in IntelliJ when DBD is present * (because ProjectDefinition can be available in recent SonarLint versions, and DBD provides a CheckRegistrar.) */ public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs, ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory, ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars, @Nullable ProjectDefinition projectDefinition, @Nullable SonarLintCache sonarLintCache) { this.fileLinesContextFactory = fileLinesContextFactory; this.fs = fs; this.javaClasspath = javaClasspath; this.javaTestClasspath = javaTestClasspath; this.checkFactory = checkFactory; this.activeRules = activeRules; this.projectDefinition = projectDefinition; this.sonarLintCache = sonarLintCache; this.mainChecks = new ArrayList<>(); this.testChecks = new ArrayList<>(); this.jspChecks = new ArrayList<>(); this.allChecks = new ArrayList<>(); this.additionalAutoScanCompatibleRuleKeys = new TreeSet<>(); if (checkRegistrars != null) { for (CheckRegistrar registrar : checkRegistrars) { registrar.register(this, checkFactory); } } } public void setSensorContext(SensorContext context) { this.context = context; } public void setCheckFilter(UnaryOperator> checkFilter) { this.checkFilter = checkFilter; } public FileLinesContext fileLinesContextFor(InputFile inputFile) { return fileLinesContextFactory.createFor(inputFile); } public NewSymbolTable symbolizableFor(InputFile inputFile) { return context.newSymbolTable().onFile(inputFile); } public NewHighlighting highlightableFor(InputFile inputFile) { Objects.requireNonNull(context); return context.newHighlighting().onFile(inputFile); } public List getJavaClasspath() { if (javaClasspath == null) { return new ArrayList<>(); } return javaClasspath.getElements(); } public boolean inAndroidContext() { return javaClasspath.inAndroidContext(); } public List getJavaTestClasspath() { return javaTestClasspath.getElements(); } public List getJspClasspath() { List jspClasspath = new ArrayList<>(); // sonar-java jar is added to classpath in order to have semantic information on code generated from JSP files jspClasspath.add(findPluginJar()); jspClasspath.addAll(getJavaClasspath()); return jspClasspath; } /** * @return the jar of sonar-java plugin */ private static File findPluginJar() { try { return new File(SonarComponents.class.getProtectionDomain().getCodeSource().getLocation().toURI()); } catch (URISyntaxException e) { // this should not happen under normal circumstances, and if it does we want to be aware of it throw new IllegalStateException("Failed to obtain plugin jar.", e); } } @Override public void registerMainChecks(String repositoryKey, Collection javaCheckClassesAndInstances) { registerCheckClasses(mainChecks, getCreatedCheckFromFactory(repositoryKey, javaCheckClassesAndInstances), javaCheckClassesAndInstances); } @Override public void registerMainChecks(Checks checks, Collection javaCheckClassesAndInstances){ registerCheckClasses(mainChecks, checks, javaCheckClassesAndInstances); } @Override public void registerTestChecks(String repositoryKey, Collection javaCheckClassesAndInstances) { registerCheckClasses(testChecks, getCreatedCheckFromFactory(repositoryKey, javaCheckClassesAndInstances), javaCheckClassesAndInstances); } @Override public void registerMainSharedCheck(JavaCheck check, Collection ruleKeys) { if (hasAtLeastOneActiveRule(ruleKeys)) { mainChecks.add(check); } } @Override public void registerTestSharedCheck(JavaCheck check, Collection ruleKeys) { if (hasAtLeastOneActiveRule(ruleKeys)) { testChecks.add(check); } } @Override public void registerAutoScanCompatibleRules(Collection ruleKeys) { additionalAutoScanCompatibleRuleKeys.addAll(ruleKeys); } public Set getAdditionalAutoScanCompatibleRuleKeys() { return additionalAutoScanCompatibleRuleKeys; } private boolean hasAtLeastOneActiveRule(Collection ruleKeys) { return ruleKeys.stream().anyMatch(ruleKey -> activeRules.find(ruleKey) != null); } private Checks getCreatedCheckFromFactory(String repositoryKey, Collection javaCheckClassesAndInstances){ return checkFactory.create(repositoryKey).addAnnotatedChecks(javaCheckClassesAndInstances); } private void registerCheckClasses(List destinationList, Checks createdChecks, Collection javaCheckClassesAndInstances) { allChecks.add(createdChecks); Map, Integer> classIndexes = new HashMap<>(); int i = 0; for (Object javaCheckClassOrInstance : javaCheckClassesAndInstances) { if (javaCheckClassOrInstance instanceof Class) { classIndexes.put((Class) javaCheckClassOrInstance, i); } else { classIndexes.put(((JavaCheck) javaCheckClassOrInstance).getClass(), i); } i++; } List orderedChecks = createdChecks.all().stream() .sorted(Comparator.comparing(check -> classIndexes.getOrDefault(check.getClass(), Integer.MAX_VALUE))) .toList(); destinationList.addAll(orderedChecks); if (LOG.isDebugEnabled()) { LOG.debug("Registered check: [{}]", orderedChecks.stream() .map(c -> c.getClass().getSimpleName() + " (" + createdChecks.ruleKey(c) + ")") .collect(Collectors.joining(", "))); } jspChecks.addAll(orderedChecks.stream().filter(JspCodeVisitor.class::isInstance).toList()); } public List mainChecks() { return checkFilter.apply(mainChecks); } public List testChecks() { return checkFilter.apply(testChecks); } public List jspChecks() { return checkFilter.apply(jspChecks); } public Optional getRuleKey(JavaCheck check) { return allChecks.stream() .map(sonarChecks -> sonarChecks.ruleKey(check)) .filter(Objects::nonNull) .findFirst(); } public void addIssue(InputComponent inputComponent, JavaCheck check, int line, String message, @Nullable Integer cost) { reportIssue(new AnalyzerMessage(check, inputComponent, line, message, cost != null ? cost.intValue() : 0)); } public void reportIssue(AnalyzerMessage analyzerMessage) { JavaCheck check = analyzerMessage.getCheck(); Objects.requireNonNull(check); Objects.requireNonNull(analyzerMessage.getMessage()); getRuleKey(check).ifPresent(key -> { InputComponent inputComponent = analyzerMessage.getInputComponent(); if (inputComponent == null) { return; } Double cost = analyzerMessage.getCost(); reportIssue(analyzerMessage, key, inputComponent, cost); }); } @VisibleForTesting void reportIssue(AnalyzerMessage analyzerMessage, RuleKey key, InputComponent fileOrProject, @Nullable Double cost) { Objects.requireNonNull(context); JavaIssue issue = JavaIssue.create(context, key, cost); AnalyzerMessage.TextSpan textSpan = analyzerMessage.primaryLocation(); if (textSpan == null) { // either an issue at file or project level issue.setPrimaryLocationOnComponent(fileOrProject, analyzerMessage.getMessage()); } else { if (!textSpan.onLine()) { Preconditions.checkState(!textSpan.isEmpty(), "Issue location should not be empty"); } issue.setPrimaryLocation((InputFile) fileOrProject, analyzerMessage.getMessage(), textSpan.startLine, textSpan.startCharacter, textSpan.endLine, textSpan.endCharacter); } if (!analyzerMessage.flows.isEmpty()) { issue.addFlow((InputFile) analyzerMessage.getInputComponent(), analyzerMessage.flows); } issue.save(); } public boolean reportAnalysisError(RecognitionException re, InputFile inputFile) { reportAnalysisError(inputFile, re.getMessage()); return isSonarLintContext(); } private void reportAnalysisError(InputFile inputFile, String message) { context.newAnalysisError() .onFile(inputFile) .message(message) .save(); } public boolean isSonarLintContext() { return context.runtime().getProduct() == SonarProduct.SONARLINT; } public boolean isQuickFixCompatible() { return isSonarLintContext() && ((SonarLintRuntime) context.runtime()).getSonarLintPluginApiVersion().isGreaterThanOrEqual(SONARLINT_6_3); } public boolean isSetQuickFixAvailableCompatible() { return context.runtime().getProduct() == SonarProduct.SONARQUBE && context.runtime().getApiVersion().isGreaterThanOrEqual(SONARQUBE_9_2); } public List fileLines(InputFile inputFile) { return LineUtils.splitLines(inputFileContents(inputFile)); } public String inputFileContents(InputFile inputFile) { try { return inputFile.contents(); } catch (IOException e) { throw new AnalysisException(String.format("Unable to read file '%s'", inputFile), e); } } public boolean analysisCancelled() { return context.isCancelled(); } public boolean shouldFailAnalysisOnException() { return context.config().getBoolean(FAIL_ON_EXCEPTION_KEY).orElse(false); } public boolean isFileByFileEnabled() { return context.config().getBoolean(SONAR_FILE_BY_FILE).orElse(false); } public boolean isAutoScan() { return (context.config().getBoolean(SONAR_BATCH_MODE_KEY).orElse(false) || context.config().getBoolean(SONAR_AUTOSCAN).orElse(false)) && !context.config().hasKey(SONAR_BATCH_SIZE_KEY); } public boolean isAutoScanCheckFiltering() { return isAutoScan() && context.config().getBoolean(SONAR_AUTOSCAN_CHECK_FILTERING).orElse(true); } /** * Returns the batch mode size as read from configuration, in Kilo Bytes. If not value can be found, compute dynamically an ideal value. * * @return the batch mode size or a default value of -1L. */ public long getBatchModeSizeInKB() { Configuration config = context.config(); if (isAutoScan()) { return -1L; } return config.getLong(SONAR_BATCH_SIZE_KEY).orElse(computeIdealBatchSize()); } public boolean shouldIgnoreUnnamedModuleForSplitPackage() { return context.config().getBoolean(SONAR_IGNORE_UNNAMED_MODULE_FOR_SPLIT_PACKAGE).orElse(false); } private static long computeIdealBatchSize() { // We take a fraction of the total memory available though -Xmx. // If we assume that the average size of a file is 5KB and the average CI should have 1GB of memory, // it will be able to analyze 10 files in batch. // We max the value to 500KB (100 files) because there is only little advantages to go further. return Math.min(500L, ((long) (maxMemoryInBytesProvider.getAsLong() * 0.00005)) / 1000L); } public File projectLevelWorkDir() { var root = getRootProject(); if (root != null) { return root.getWorkDir(); } else { return fs.workDir(); } } /** * Returns an OS-independent key that should identify the module within the project * * @return A key representing the module */ public String getModuleKey() { var root = getRootProject(); if (root != null && projectDefinition != null) { var rootBase = root.getBaseDir().toPath(); var moduleBase = projectDefinition.getBaseDir().toPath(); return rootBase.relativize(moduleBase).toString().replace('\\', '/'); } return ""; } @CheckForNull private ProjectDefinition getRootProject() { ProjectDefinition current = projectDefinition; if (current == null) { return null; } while (current.getParent() != null) { current = current.getParent(); } return current; } public boolean canSkipUnchangedFiles() throws ApiMismatchException { if (context == null) { return false; } else { var overrideSkipFlag = context.config() == null ? null : context.config().getBoolean(SONAR_CAN_SKIP_UNCHANGED_FILES_KEY).orElse(null); try { if (overrideSkipFlag != null) { return overrideSkipFlag; } Method canSkipUnchangedFiles = context.getClass().getMethod("canSkipUnchangedFiles"); return (Boolean) canSkipUnchangedFiles.invoke(context); } catch (NoSuchMethodError | NoSuchMethodException error) { throw new ApiMismatchException(error); } catch (InvocationTargetException | IllegalAccessException error) { Throwable cause = error.getCause(); if (cause instanceof NoSuchMethodError) { throw new ApiMismatchException(cause); } throw new ApiMismatchException(error); } } } public boolean fileCanBeSkipped(InputFile inputFile) { var contentHashCache = new ContentHashCache(this); if (inputFile instanceof GeneratedFile) { // Generated files should not be skipped as we cannot assess the change status of the source file return false; } boolean canSkipInContext; try { canSkipInContext = canSkipUnchangedFiles(); if (!alreadyLoggedSkipStatus) { if (canSkipInContext) { LOG.info("The Java analyzer is running in a context where unchanged files can be skipped. Full analysis is performed " + "for changed files, optimized analysis for unchanged files."); } else { LOG.info("The Java analyzer cannot skip unchanged files in this context. A full analysis is performed for all files."); } alreadyLoggedSkipStatus = true; } } catch (ApiMismatchException e) { if (!alreadyLoggedSkipStatus) { LOG.info( "Cannot determine whether the context allows skipping unchanged files: canSkipUnchangedFiles not part of sonar-plugin-api. " + "Not skipping. {}", e.getCause().getMessage() ); alreadyLoggedSkipStatus = true; } contentHashCache.writeToCache(inputFile); return false; } if (!canSkipInContext) { contentHashCache.writeToCache(inputFile); return false; } return contentHashCache.hasSameHashCached(inputFile); } public InputComponent project() { return context.project(); } public void collectUndefinedTypes(String pathToFile, Set undefinedTypes) { undefinedTypes.stream().forEach(problem -> { List filesAffectedByProblem = problemsToFilePaths.computeIfAbsent(problem, key -> new ArrayList<>()); filesAffectedByProblem.add(pathToFile); }); } public void logUndefinedTypes() { if (problemsToFilePaths.isEmpty()) { return; } javaClasspath.logSuspiciousEmptyLibraries(); if (!isAutoScan()) { // In autoscan, test + main code are analyzed in the same batch, and we do not make the distinction between // test and main libraries, everything is inside "sonar.java.libraries", it is expected to let the test property empty. javaTestClasspath.logSuspiciousEmptyLibraries(); } logUndefinedTypes(LOGGED_MAX_NUMBER_UNDEFINED_TYPES); // clear the set so only new undefined types will be logged problemsToFilePaths.clear(); } private void logUndefinedTypes(int maxLines) { logParserMessages( problemsToFilePaths.entrySet().stream() .filter(entry -> entry.getKey().type() == JProblem.Type.UNDEFINED_TYPE), maxLines, "Unresolved imports/types have been detected during analysis. Enable DEBUG mode to see them.", "Unresolved imports/types:" ); logParserMessages( problemsToFilePaths.entrySet().stream() .filter(entry -> entry.getKey().type() == JProblem.Type.PREVIEW_FEATURE_USED), maxLines, "Use of preview features have been detected during analysis. Enable DEBUG mode to see them.", "Use of preview features:" ); } private static void logParserMessages(Stream>> messages, int maxProblems, String warningMessage, String debugMessage) { String problemDelimiter = System.lineSeparator() + "- "; List> messagesList = messages .sorted(Comparator.comparing(entry -> entry.getKey().toString())) // We only consider the first `maxProblems` elements. We keep an extra one to know if we passed the threshold in later tests. .limit(maxProblems + 1L) .map(entry -> { List paths = entry.getValue(); List problemAndPaths = new ArrayList<>(paths.size() + 1); problemAndPaths.add(problemDelimiter + entry.getKey().toString()); paths.forEach(path -> problemAndPaths.add(" * " + path)); return problemAndPaths; }) .toList(); if (messagesList.isEmpty()) { return; } LOG.warn(warningMessage); if (LOG.isDebugEnabled()) { boolean moreThanMax = messagesList.size() > maxProblems; String firstLine = moreThanMax ? (debugMessage + " (Limited to " + maxProblems + ")") : debugMessage; String lastLine = moreThanMax ? (System.lineSeparator() + problemDelimiter + "...") : ""; LOG.debug(messagesList .stream() .limit(maxProblems) .flatMap(List::stream) .collect(Collectors.joining(System.lineSeparator(), firstLine, lastLine)) ); } } public SensorContext context() { return context; } @CheckForNull public SonarLintCache sonarLintCache() { return sonarLintCache; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy