org.sonar.plugins.javascript.bridge.TsConfigProvider Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sonar-javascript-plugin Show documentation
Show all versions of sonar-javascript-plugin Show documentation
Code Analyzer for JavaScript/TypeScript/CSS
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-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.plugins.javascript.bridge;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import com.google.gson.Gson;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.SonarProduct;
import org.sonar.api.batch.fs.FilePredicate;
import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.plugins.javascript.JavaScriptFilePredicate;
import org.sonar.plugins.javascript.JavaScriptPlugin;
import org.sonar.plugins.javascript.sonarlint.SonarLintTypeCheckingChecker;
import org.sonarsource.analyzer.commons.FileProvider;
class TsConfigProvider {
private static final Logger LOG = LoggerFactory.getLogger(TsConfigProvider.class);
interface Provider {
List tsconfigs(SensorContext context) throws IOException;
}
@FunctionalInterface
interface TsConfigFileCreator {
String createTsConfigFile(String content) throws IOException;
}
private final List providers;
private TsConfigProvider(List providers) {
this.providers = providers;
}
/**
* Relying on (in order of priority)
* 1. Property sonar.typescript.tsconfigPath(s)
* 2. Looking up file system
* 3. Creating a tmp tsconfig.json listing all files
*/
static List getTsConfigs(
ContextUtils contextUtils,
@Nullable SonarLintTypeCheckingChecker javaScriptProjectChecker,
TsConfigFileCreator tsConfigFileCreator
) throws IOException {
var defaultProvider = contextUtils.isSonarLint()
? new TsConfigProvider.WildcardTsConfigProvider(javaScriptProjectChecker, tsConfigFileCreator)
: new DefaultTsConfigProvider(tsConfigFileCreator, JavaScriptFilePredicate::getJsTsPredicate);
var provider = new TsConfigProvider(
List.of(new PropertyTsConfigProvider(), new LookupTsConfigProvider(), defaultProvider)
);
return provider.tsconfigs(contextUtils.context());
}
List tsconfigs(SensorContext context) throws IOException {
for (Provider provider : providers) {
List tsconfigs = provider.tsconfigs(context);
if (!tsconfigs.isEmpty()) {
return tsconfigs;
}
}
return emptyList();
}
static class PropertyTsConfigProvider implements Provider {
@Override
public List tsconfigs(SensorContext context) {
if (
!context.config().hasKey(JavaScriptPlugin.TSCONFIG_PATHS) &&
!context.config().hasKey(JavaScriptPlugin.TSCONFIG_PATHS_ALIAS)
) {
return emptyList();
}
String property = context.config().hasKey(JavaScriptPlugin.TSCONFIG_PATHS)
? JavaScriptPlugin.TSCONFIG_PATHS
: JavaScriptPlugin.TSCONFIG_PATHS_ALIAS;
Set patterns = new HashSet<>(
Arrays.asList(context.config().getStringArray(property))
);
LOG.info(
"Resolving TSConfig files using '{}' from property {}",
String.join(",", patterns),
property
);
File baseDir = context.fileSystem().baseDir();
List tsconfigs = new ArrayList<>();
for (String pattern : patterns) {
LOG.debug("Using '{}' to resolve TSConfig file(s)", pattern);
/** Resolving a TSConfig file based on a path */
Path tsconfig = getFilePath(baseDir, pattern);
if (tsconfig != null) {
tsconfigs.add(tsconfig.toString());
continue;
}
/** Resolving TSConfig files based on pattern matching */
FileProvider fileProvider = new FileProvider(baseDir, pattern);
List matchingTsconfigs = fileProvider.getMatchingFiles();
if (!matchingTsconfigs.isEmpty()) {
tsconfigs.addAll(matchingTsconfigs.stream().map(File::getAbsolutePath).toList());
}
}
LOG.info("Found " + tsconfigs.size() + " TSConfig file(s): " + tsconfigs);
return tsconfigs;
}
private static Path getFilePath(File baseDir, String path) {
File file = new File(path);
if (!file.isAbsolute()) {
file = new File(baseDir, path);
}
if (!file.isFile()) {
return null;
}
return file.toPath();
}
}
static class LookupTsConfigProvider implements Provider {
@Override
public List tsconfigs(SensorContext context) {
var fs = context.fileSystem();
var tsconfigs = new ArrayList();
var dirs = new ArrayDeque();
dirs.add(fs.baseDir());
while (!dirs.isEmpty()) {
var dir = dirs.removeFirst();
var files = dir.listFiles();
if (files == null) {
continue;
}
for (var file : files) {
if (file.isDirectory() && !"node_modules".equals(file.getName())) {
dirs.add(file);
} else if ("tsconfig.json".equals(file.getName())) {
tsconfigs.add(file.getAbsolutePath());
}
}
}
LOG.info("Found {} tsconfig.json file(s): {}",tsconfigs.size(), tsconfigs);
return tsconfigs;
}
}
abstract static class GeneratedTsConfigFileProvider implements Provider {
static class TsConfig {
List files;
Map compilerOptions = new LinkedHashMap<>();
List include;
TsConfig(@Nullable Iterable inputFiles, @Nullable List include) {
compilerOptions.put("allowJs", true);
compilerOptions.put("noImplicitAny", true);
if (inputFiles != null) {
files = new ArrayList<>();
inputFiles.forEach(f -> files.add(f.absolutePath()));
}
this.include = include;
}
List writeFileWith(TsConfigFileCreator tsConfigFileCreator) {
try {
return singletonList(tsConfigFileCreator.createTsConfigFile(new Gson().toJson(this)));
} catch (IOException e) {
LOG.warn("Generating tsconfig.json failed", e);
return emptyList();
}
}
}
final SonarProduct product;
GeneratedTsConfigFileProvider(SonarProduct product) {
this.product = product;
}
@Override
public final List tsconfigs(SensorContext context) throws IOException {
if (context.runtime().getProduct() != product) {
// we don't support per analysis temporary files in SonarLint see https://jira.sonarsource.com/browse/SLCORE-235
LOG.warn(
"Generating temporary tsconfig is not supported by {} in {} context.",
getClass().getSimpleName(),
context.runtime().getProduct()
);
return emptyList();
}
return getDefaultTsConfigs(context);
}
abstract List getDefaultTsConfigs(SensorContext context) throws IOException;
}
static class DefaultTsConfigProvider extends GeneratedTsConfigFileProvider {
private final Function filePredicateProvider;
private final TsConfigFileCreator tsConfigFileCreator;
DefaultTsConfigProvider(
TsConfigFileCreator tsConfigFileCreator,
Function filePredicate
) {
super(SonarProduct.SONARQUBE);
this.tsConfigFileCreator = tsConfigFileCreator;
this.filePredicateProvider = filePredicate;
}
@Override
List getDefaultTsConfigs(SensorContext context) throws IOException {
var inputFiles = context
.fileSystem()
.inputFiles(filePredicateProvider.apply(context.fileSystem()));
var tsConfig = new TsConfig(inputFiles, null);
var tsconfigFile = writeToJsonFile(tsConfig);
LOG.debug("Using generated tsconfig.json file {}", tsconfigFile.getAbsolutePath());
return singletonList(tsconfigFile.getAbsolutePath());
}
private File writeToJsonFile(TsConfig tsConfig) throws IOException {
String json = new Gson().toJson(tsConfig);
return Path.of(tsConfigFileCreator.createTsConfigFile(json)).toFile();
}
}
static class WildcardTsConfigProvider extends GeneratedTsConfigFileProvider {
private static String getProjectRoot(SensorContext context) {
var projectBaseDir = context.fileSystem().baseDir().getAbsolutePath();
return "/".equals(File.separator)
? projectBaseDir
: projectBaseDir.replace(File.separator, "/");
}
private static final Map> defaultWildcardTsConfig =
new ConcurrentHashMap<>();
final TsConfigFileCreator tsConfigFileCreator;
private final boolean deactivated;
WildcardTsConfigProvider(
@Nullable SonarLintTypeCheckingChecker checker,
TsConfigFileCreator tsConfigFileCreator
) {
super(SonarProduct.SONARLINT);
this.tsConfigFileCreator = tsConfigFileCreator;
deactivated = checker == null || checker.isBeyondLimit();
}
@Override
List getDefaultTsConfigs(SensorContext context) {
if (deactivated) {
return emptyList();
} else {
return defaultWildcardTsConfig.computeIfAbsent(
getProjectRoot(context),
this::writeTsConfigFileFor
);
}
}
List writeTsConfigFileFor(String root) {
var config = new TsConfig(null, singletonList(root + "/**/*"));
var file = config.writeFileWith(tsConfigFileCreator);
LOG.debug("Using generated tsconfig.json file using wildcards {}", file);
return file;
}
}
}