diff options
author | Duarte Meneses <duarte.meneses@sonarsource.com> | 2019-06-04 09:24:58 -0500 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-07-12 20:21:13 +0200 |
commit | e3a0108ef0dd26689dc58198e85ba8655c77fb1f (patch) | |
tree | d2cedb4bfd8cb6ac737420b8f45713475871ee89 /sonar-scanner-engine/src | |
parent | 0f63c1e2bdf0c9fad3ca66ea64ef197bb16e6258 (diff) | |
download | sonarqube-e3a0108ef0dd26689dc58198e85ba8655c77fb1f.tar.gz sonarqube-e3a0108ef0dd26689dc58198e85ba8655c77fb1f.zip |
Extract implementation from plugin API - Scanner FS
Diffstat (limited to 'sonar-scanner-engine/src')
58 files changed, 4836 insertions, 10 deletions
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/AbstractProjectOrModule.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/AbstractProjectOrModule.java new file mode 100644 index 00000000000..9fc2d130f4a --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/AbstractProjectOrModule.java @@ -0,0 +1,161 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.CheckForNull; +import javax.annotation.concurrent.Immutable; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.SystemUtils; +import org.sonar.api.CoreProperties; +import org.sonar.api.batch.bootstrap.ProjectDefinition; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +@Immutable +public abstract class AbstractProjectOrModule extends DefaultInputComponent { + private static final Logger LOGGER = Loggers.get(AbstractProjectOrModule.class); + private final Path baseDir; + private final Path workDir; + private final String name; + private final String originalName; + private final String description; + private final String keyWithBranch; + private final String branch; + private final Map<String, String> properties; + + private final String key; + private final ProjectDefinition definition; + private final Charset encoding; + + public AbstractProjectOrModule(ProjectDefinition definition, int scannerComponentId) { + super(scannerComponentId); + this.baseDir = initBaseDir(definition); + this.workDir = initWorkingDir(definition); + this.name = definition.getName(); + this.originalName = definition.getOriginalName(); + this.description = definition.getDescription(); + this.keyWithBranch = definition.getKeyWithBranch(); + this.branch = definition.getBranch(); + this.properties = Collections.unmodifiableMap(new HashMap<>(definition.properties())); + + this.definition = definition; + this.key = definition.getKey(); + this.encoding = initEncoding(definition); + } + + private static Charset initEncoding(ProjectDefinition module) { + String encodingStr = module.properties().get(CoreProperties.ENCODING_PROPERTY); + Charset result; + if (StringUtils.isNotEmpty(encodingStr)) { + result = Charset.forName(StringUtils.trim(encodingStr)); + } else { + result = Charset.defaultCharset(); + } + return result; + } + + private static Path initBaseDir(ProjectDefinition module) { + Path result; + try { + result = module.getBaseDir().toPath().toRealPath(LinkOption.NOFOLLOW_LINKS); + } catch (IOException e) { + throw new IllegalStateException("Unable to resolve module baseDir", e); + } + return result; + } + + private static Path initWorkingDir(ProjectDefinition module) { + File workingDirAsFile = module.getWorkDir(); + Path workingDir = workingDirAsFile.getAbsoluteFile().toPath().normalize(); + if (SystemUtils.IS_OS_WINDOWS) { + try { + Files.createDirectories(workingDir); + Files.setAttribute(workingDir, "dos:hidden", true, LinkOption.NOFOLLOW_LINKS); + } catch (IOException e) { + LOGGER.warn("Failed to set working directory hidden: {}", e.getMessage()); + } + } + return workingDir; + } + + /** + * Module key without branch + */ + @Override + public String key() { + return key; + } + + @Override + public boolean isFile() { + return false; + } + + public ProjectDefinition definition() { + return definition; + } + + public Path getBaseDir() { + return baseDir; + } + + public Path getWorkDir() { + return workDir; + } + + public String getKeyWithBranch() { + return keyWithBranch; + } + + @CheckForNull + public String getBranch() { + return branch; + } + + public Map<String, String> properties() { + return properties; + } + + @CheckForNull + public String getOriginalName() { + return originalName; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Charset getEncoding() { + return encoding; + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultFileSystem.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultFileSystem.java new file mode 100644 index 00000000000..1c1729634e1 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultFileSystem.java @@ -0,0 +1,246 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.stream.StreamSupport; +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.FilePredicates; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputDir; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.scan.filesystem.PathResolver; +import org.sonar.api.utils.PathUtils; +import org.sonar.scanner.fs.predicates.DefaultFilePredicates; +import org.sonar.scanner.fs.predicates.FileExtensionPredicate; +import org.sonar.scanner.fs.predicates.OptimizedFilePredicateAdapter; + +/** + * @since 4.2 + */ +public class DefaultFileSystem implements FileSystem { + + private final Cache cache; + private final Path baseDir; + private Path workDir; + private Charset encoding; + protected final FilePredicates predicates; + + /** + * Only for testing + */ + public DefaultFileSystem(Path baseDir) { + this(baseDir, new MapCache()); + } + + /** + * Only for testing + */ + public DefaultFileSystem(File baseDir) { + this(baseDir.toPath(), new MapCache()); + } + + protected DefaultFileSystem(Path baseDir, Cache cache) { + this.baseDir = baseDir; + this.cache = cache; + this.predicates = new DefaultFilePredicates(this.baseDir); + } + + public Path baseDirPath() { + return baseDir; + } + + @Override + public File baseDir() { + return baseDir.toFile(); + } + + public DefaultFileSystem setEncoding(Charset e) { + this.encoding = e; + return this; + } + + @Override + public Charset encoding() { + return encoding; + } + + public DefaultFileSystem setWorkDir(Path d) { + this.workDir = d; + return this; + } + + @Override + public File workDir() { + return workDir.toFile(); + } + + @Override + public InputFile inputFile(FilePredicate predicate) { + Iterable<InputFile> files = inputFiles(predicate); + Iterator<InputFile> iterator = files.iterator(); + if (!iterator.hasNext()) { + return null; + } + InputFile first = iterator.next(); + if (!iterator.hasNext()) { + return first; + } + + StringBuilder sb = new StringBuilder(); + sb.append("expected one element but was: <" + first); + for (int i = 0; i < 4 && iterator.hasNext(); i++) { + sb.append(", " + iterator.next()); + } + if (iterator.hasNext()) { + sb.append(", ..."); + } + sb.append('>'); + + throw new IllegalArgumentException(sb.toString()); + + } + + public Iterable<InputFile> inputFiles() { + return inputFiles(predicates.all()); + } + + @Override + public Iterable<InputFile> inputFiles(FilePredicate predicate) { + return OptimizedFilePredicateAdapter.create(predicate).get(cache); + } + + @Override + public boolean hasFiles(FilePredicate predicate) { + return inputFiles(predicate).iterator().hasNext(); + } + + @Override + public Iterable<File> files(FilePredicate predicate) { + return () -> StreamSupport.stream(inputFiles(predicate).spliterator(), false) + .map(InputFile::file) + .iterator(); + } + + @Override + public InputDir inputDir(File dir) { + String relativePath = PathUtils.sanitize(new PathResolver().relativePath(baseDir.toFile(), dir)); + if (relativePath == null) { + return null; + } + // Issues on InputDir are moved to the project, so we just return a fake InputDir for backward compatibility + return new DefaultInputDir("unused", relativePath).setModuleBaseDir(baseDir); + } + + public DefaultFileSystem add(InputFile inputFile) { + cache.add(inputFile); + return this; + } + + @Override + public SortedSet<String> languages() { + return cache.languages(); + } + + @Override + public FilePredicates predicates() { + return predicates; + } + + public abstract static class Cache implements Index { + + protected abstract void doAdd(InputFile inputFile); + + final void add(InputFile inputFile) { + doAdd(inputFile); + } + + protected abstract SortedSet<String> languages(); + } + + /** + * Used only for testing + */ + private static class MapCache extends Cache { + private final Map<String, InputFile> fileMap = new HashMap<>(); + private final Map<String, Set<InputFile>> filesByNameCache = new HashMap<>(); + private final Map<String, Set<InputFile>> filesByExtensionCache = new HashMap<>(); + private SortedSet<String> languages = new TreeSet<>(); + + @Override + public Iterable<InputFile> inputFiles() { + return new ArrayList<>(fileMap.values()); + } + + @Override + public InputFile inputFile(String relativePath) { + return fileMap.get(relativePath); + } + + @Override + public Iterable<InputFile> getFilesByName(String filename) { + return filesByNameCache.get(filename); + } + + @Override + public Iterable<InputFile> getFilesByExtension(String extension) { + return filesByExtensionCache.get(extension); + } + + @Override + protected void doAdd(InputFile inputFile) { + if (inputFile.language() != null) { + languages.add(inputFile.language()); + } + fileMap.put(inputFile.relativePath(), inputFile); + filesByNameCache.computeIfAbsent(inputFile.filename(), x -> new HashSet<>()).add(inputFile); + filesByExtensionCache.computeIfAbsent(FileExtensionPredicate.getExtension(inputFile), x -> new HashSet<>()).add(inputFile); + } + + @Override + protected SortedSet<String> languages() { + return languages; + } + } + + @Override + public File resolvePath(String path) { + File file = new File(path); + if (!file.isAbsolute()) { + try { + file = new File(baseDir(), path).getCanonicalFile(); + } catch (IOException e) { + throw new IllegalArgumentException("Unable to resolve path '" + path + "'", e); + } + } + return file; + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultIndexedFile.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultIndexedFile.java new file mode 100644 index 00000000000..a3d060217e0 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultIndexedFile.java @@ -0,0 +1,158 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.batch.fs.IndexedFile; +import org.sonar.api.batch.fs.InputFile.Type; +import org.sonar.api.utils.PathUtils; + +/** + * @since 6.3 + */ +@Immutable +public class DefaultIndexedFile extends DefaultInputComponent implements IndexedFile { + private final String projectRelativePath; + private final String moduleRelativePath; + private final String projectKey; + private final String language; + private final Type type; + private final Path absolutePath; + private final SensorStrategy sensorStrategy; + + /** + * Testing purposes only! + */ + public DefaultIndexedFile(String projectKey, Path baseDir, String relativePath, @Nullable String language) { + this(baseDir.resolve(relativePath), projectKey, relativePath, relativePath, Type.MAIN, language, TestInputFileBuilder.nextBatchId(), + new SensorStrategy()); + } + + public DefaultIndexedFile(Path absolutePath, String projectKey, String projectRelativePath, String moduleRelativePath, Type type, @Nullable String language, int batchId, + SensorStrategy sensorStrategy) { + super(batchId); + this.projectKey = projectKey; + this.projectRelativePath = PathUtils.sanitize(projectRelativePath); + this.moduleRelativePath = PathUtils.sanitize(moduleRelativePath); + this.type = type; + this.language = language; + this.sensorStrategy = sensorStrategy; + this.absolutePath = absolutePath; + } + + @Override + public String relativePath() { + return sensorStrategy.isGlobal() ? projectRelativePath : moduleRelativePath; + } + + public String getModuleRelativePath() { + return moduleRelativePath; + } + + public String getProjectRelativePath() { + return projectRelativePath; + } + + @Override + public String absolutePath() { + return PathUtils.sanitize(path().toString()); + } + + @Override + public File file() { + return path().toFile(); + } + + @Override + public Path path() { + return absolutePath; + } + + @Override + public InputStream inputStream() throws IOException { + return Files.newInputStream(path()); + } + + @CheckForNull + @Override + public String language() { + return language; + } + + @Override + public Type type() { + return type; + } + + /** + * Component key (without branch). + */ + @Override + public String key() { + return new StringBuilder().append(projectKey).append(":").append(projectRelativePath).toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof DefaultIndexedFile)) { + return false; + } + + DefaultIndexedFile that = (DefaultIndexedFile) o; + return projectRelativePath.equals(that.projectRelativePath); + } + + @Override + public int hashCode() { + return projectRelativePath.hashCode(); + } + + @Override + public String toString() { + return projectRelativePath; + } + + @Override + public boolean isFile() { + return true; + } + + @Override + public String filename() { + return path().getFileName().toString(); + } + + @Override + public URI uri() { + return path().toUri(); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultInputComponent.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultInputComponent.java new file mode 100644 index 00000000000..11601e0265f --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultInputComponent.java @@ -0,0 +1,72 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs; + +import java.util.HashSet; +import java.util.Set; +import org.sonar.api.batch.fs.InputComponent; +import org.sonar.api.batch.measure.Metric; + +/** + * @since 5.2 + */ +public abstract class DefaultInputComponent implements InputComponent { + private int id; + private Set<String> storedMetricKeys = new HashSet<>(); + + public DefaultInputComponent(int scannerId) { + this.id = scannerId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || this.getClass() != o.getClass()) { + return false; + } + + DefaultInputComponent that = (DefaultInputComponent) o; + return key().equals(that.key()); + } + + public int scannerId() { + return id; + } + + @Override + public int hashCode() { + return key().hashCode(); + } + + @Override + public String toString() { + return "[key=" + key() + "]"; + } + + public void setHasMeasureFor(Metric metric) { + storedMetricKeys.add(metric.key()); + } + + public boolean hasMeasureFor(Metric metric) { + return storedMetricKeys.contains(metric.key()); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultInputDir.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultInputDir.java new file mode 100644 index 00000000000..13eb77be1c4 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultInputDir.java @@ -0,0 +1,122 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs; + +import java.io.File; +import java.net.URI; +import java.nio.file.Path; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.batch.fs.InputDir; +import org.sonar.api.utils.PathUtils; + +/** + * @since 4.5 + */ +public class DefaultInputDir extends DefaultInputComponent implements InputDir { + + private final String relativePath; + private final String moduleKey; + private Path moduleBaseDir; + + public DefaultInputDir(String moduleKey, String relativePath) { + super(-1); + this.moduleKey = moduleKey; + this.relativePath = PathUtils.sanitize(relativePath); + } + + @Override + public String relativePath() { + return relativePath; + } + + @Override + public String absolutePath() { + return PathUtils.sanitize(path().toString()); + } + + @Override + public File file() { + return path().toFile(); + } + + @Override + public Path path() { + if (moduleBaseDir == null) { + throw new IllegalStateException("Can not return the java.nio.file.Path because module baseDir is not set (see method setModuleBaseDir(java.io.File))"); + } + return moduleBaseDir.resolve(relativePath); + } + + public String moduleKey() { + return moduleKey; + } + + @Override + public String key() { + StringBuilder sb = new StringBuilder().append(moduleKey).append(":"); + if (StringUtils.isEmpty(relativePath)) { + sb.append("/"); + } else { + sb.append(relativePath); + } + return sb.toString(); + } + + /** + * For testing purpose. Will be automatically set when dir is added to {@link DefaultFileSystem} + */ + public DefaultInputDir setModuleBaseDir(Path moduleBaseDir) { + this.moduleBaseDir = moduleBaseDir.normalize(); + return this; + } + + @Override + public boolean isFile() { + return false; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || this.getClass() != o.getClass()) { + return false; + } + + DefaultInputDir that = (DefaultInputDir) o; + return moduleKey.equals(that.moduleKey) && relativePath.equals(that.relativePath); + } + + @Override + public int hashCode() { + return moduleKey.hashCode() + relativePath.hashCode() * 13; + } + + @Override + public String toString() { + return "[moduleKey=" + moduleKey + ", relative=" + relativePath + ", basedir=" + moduleBaseDir + "]"; + } + + @Override + public URI uri() { + return path().toUri(); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultInputFile.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultInputFile.java new file mode 100644 index 00000000000..f29f11db64a --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultInputFile.java @@ -0,0 +1,440 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.apache.commons.io.ByteOrderMark; +import org.apache.commons.io.input.BOMInputStream; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.TextPointer; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.batch.fs.internal.Metadata; + +import static org.sonar.api.utils.Preconditions.checkArgument; +import static org.sonar.api.utils.Preconditions.checkState; + +/** + * @since 4.2 + * To create {@link InputFile} in tests, use {@link TestInputFileBuilder}. + */ +public class DefaultInputFile extends DefaultInputComponent implements InputFile { + + private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; + + private final DefaultIndexedFile indexedFile; + private final String contents; + private final Consumer<DefaultInputFile> metadataGenerator; + + private boolean published; + private boolean excludedForCoverage; + private boolean excludedForDuplication; + private boolean ignoreAllIssues; + // Lazy init to save memory + private BitSet noSonarLines; + private Status status; + private Charset charset; + private Metadata metadata; + private Collection<int[]> ignoreIssuesOnlineRanges; + private BitSet executableLines; + + public DefaultInputFile(DefaultIndexedFile indexedFile, Consumer<DefaultInputFile> metadataGenerator) { + this(indexedFile, metadataGenerator, null); + } + + // For testing + public DefaultInputFile(DefaultIndexedFile indexedFile, Consumer<DefaultInputFile> metadataGenerator, @Nullable String contents) { + super(indexedFile.scannerId()); + this.indexedFile = indexedFile; + this.metadataGenerator = metadataGenerator; + this.metadata = null; + this.published = false; + this.excludedForCoverage = false; + this.contents = contents; + } + + public void checkMetadata() { + if (metadata == null) { + metadataGenerator.accept(this); + } + } + + @Override + public InputStream inputStream() throws IOException { + return contents != null ? new ByteArrayInputStream(contents.getBytes(charset())) + : new BOMInputStream(Files.newInputStream(path()), + ByteOrderMark.UTF_8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE); + } + + @Override + public String contents() throws IOException { + if (contents != null) { + return contents; + } else { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + try (InputStream inputStream = inputStream()) { + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + int length; + while ((length = inputStream.read(buffer)) != -1) { + result.write(buffer, 0, length); + } + } + return result.toString(charset().name()); + } + } + + public DefaultInputFile setPublished(boolean published) { + this.published = published; + return this; + } + + public boolean isPublished() { + return published; + } + + public DefaultInputFile setExcludedForCoverage(boolean excludedForCoverage) { + this.excludedForCoverage = excludedForCoverage; + return this; + } + + public boolean isExcludedForCoverage() { + return excludedForCoverage; + } + + public DefaultInputFile setExcludedForDuplication(boolean excludedForDuplication) { + this.excludedForDuplication = excludedForDuplication; + return this; + } + + public boolean isExcludedForDuplication() { + return excludedForDuplication; + } + + /** + * @deprecated since 6.6 + */ + @Deprecated + @Override + public String relativePath() { + return indexedFile.relativePath(); + } + + public String getModuleRelativePath() { + return indexedFile.getModuleRelativePath(); + } + + public String getProjectRelativePath() { + return indexedFile.getProjectRelativePath(); + } + + @Override + public String absolutePath() { + return indexedFile.absolutePath(); + } + + @Override + public File file() { + return indexedFile.file(); + } + + @Override + public Path path() { + return indexedFile.path(); + } + + @CheckForNull + @Override + public String language() { + return indexedFile.language(); + } + + @Override + public Type type() { + return indexedFile.type(); + } + + /** + * Component key (without branch). + */ + @Override + public String key() { + return indexedFile.key(); + } + + @Override + public int hashCode() { + return indexedFile.hashCode(); + } + + @Override + public String toString() { + return indexedFile.toString(); + } + + /** + * {@link #setStatus(Status)} + */ + @Override + public Status status() { + checkMetadata(); + return status; + } + + @Override + public int lines() { + checkMetadata(); + return metadata.lines(); + } + + @Override + public boolean isEmpty() { + checkMetadata(); + return metadata.isEmpty(); + } + + @Override + public Charset charset() { + checkMetadata(); + return charset; + } + + public int lastValidOffset() { + checkMetadata(); + return metadata.lastValidOffset(); + } + + /** + * Digest hash of the file. + */ + public String hash() { + checkMetadata(); + return metadata.hash(); + } + + public int nonBlankLines() { + checkMetadata(); + return metadata.nonBlankLines(); + } + + public int[] originalLineStartOffsets() { + checkMetadata(); + checkState(metadata.originalLineStartOffsets() != null, "InputFile is not properly initialized."); + checkState(metadata.originalLineStartOffsets().length == metadata.lines(), + "InputFile is not properly initialized. 'originalLineStartOffsets' property length should be equal to 'lines'"); + return metadata.originalLineStartOffsets(); + } + + public int[] originalLineEndOffsets() { + checkMetadata(); + checkState(metadata.originalLineEndOffsets() != null, "InputFile is not properly initialized."); + checkState(metadata.originalLineEndOffsets().length == metadata.lines(), + "InputFile is not properly initialized. 'originalLineEndOffsets' property length should be equal to 'lines'"); + return metadata.originalLineEndOffsets(); + } + + @Override + public TextPointer newPointer(int line, int lineOffset) { + checkMetadata(); + DefaultTextPointer textPointer = new DefaultTextPointer(line, lineOffset); + checkValid(textPointer, "pointer"); + return textPointer; + } + + @Override + public TextRange newRange(TextPointer start, TextPointer end) { + checkMetadata(); + checkValid(start, "start pointer"); + checkValid(end, "end pointer"); + return newRangeValidPointers(start, end, false); + } + + @Override + public TextRange newRange(int startLine, int startLineOffset, int endLine, int endLineOffset) { + checkMetadata(); + TextPointer start = newPointer(startLine, startLineOffset); + TextPointer end = newPointer(endLine, endLineOffset); + return newRangeValidPointers(start, end, false); + } + + @Override + public TextRange selectLine(int line) { + checkMetadata(); + TextPointer startPointer = newPointer(line, 0); + TextPointer endPointer = newPointer(line, lineLength(line)); + return newRangeValidPointers(startPointer, endPointer, true); + } + + public void validate(TextRange range) { + checkMetadata(); + checkValid(range.start(), "start pointer"); + checkValid(range.end(), "end pointer"); + } + + /** + * Create Range from global offsets. Used for backward compatibility with older API. + */ + public TextRange newRange(int startOffset, int endOffset) { + checkMetadata(); + return newRangeValidPointers(newPointer(startOffset), newPointer(endOffset), false); + } + + public TextPointer newPointer(int globalOffset) { + checkMetadata(); + checkArgument(globalOffset >= 0, "%s is not a valid offset for a file", globalOffset); + checkArgument(globalOffset <= lastValidOffset(), "%s is not a valid offset for file %s. Max offset is %s", globalOffset, this, lastValidOffset()); + int line = findLine(globalOffset); + int startLineOffset = originalLineStartOffsets()[line - 1]; + // In case the global offset is between \r and \n, move the pointer to a valid location + return new DefaultTextPointer(line, Math.min(globalOffset, originalLineEndOffsets()[line - 1]) - startLineOffset); + } + + public DefaultInputFile setStatus(Status status) { + this.status = status; + return this; + } + + public DefaultInputFile setCharset(Charset charset) { + this.charset = charset; + return this; + } + + private void checkValid(TextPointer pointer, String owner) { + checkArgument(pointer.line() >= 1, "%s is not a valid line for a file", pointer.line()); + checkArgument(pointer.line() <= this.metadata.lines(), "%s is not a valid line for %s. File %s has %s line(s)", pointer.line(), owner, this, metadata.lines()); + checkArgument(pointer.lineOffset() >= 0, "%s is not a valid line offset for a file", pointer.lineOffset()); + int lineLength = lineLength(pointer.line()); + checkArgument(pointer.lineOffset() <= lineLength, + "%s is not a valid line offset for %s. File %s has %s character(s) at line %s", pointer.lineOffset(), owner, this, lineLength, pointer.line()); + } + + private int lineLength(int line) { + return originalLineEndOffsets()[line - 1] - originalLineStartOffsets()[line - 1]; + } + + private static TextRange newRangeValidPointers(TextPointer start, TextPointer end, boolean acceptEmptyRange) { + checkArgument(acceptEmptyRange ? (start.compareTo(end) <= 0) : (start.compareTo(end) < 0), + "Start pointer %s should be before end pointer %s", start, end); + return new DefaultTextRange(start, end); + } + + private int findLine(int globalOffset) { + return Math.abs(Arrays.binarySearch(originalLineStartOffsets(), globalOffset) + 1); + } + + public DefaultInputFile setMetadata(Metadata metadata) { + this.metadata = metadata; + return this; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (this.getClass() != obj.getClass()) { + return false; + } + + DefaultInputFile that = (DefaultInputFile) obj; + return this.getProjectRelativePath().equals(that.getProjectRelativePath()); + } + + @Override + public boolean isFile() { + return true; + } + + @Override + public String filename() { + return indexedFile.filename(); + } + + @Override + public URI uri() { + return indexedFile.uri(); + } + + public void noSonarAt(Set<Integer> noSonarLines) { + if (this.noSonarLines == null) { + this.noSonarLines = new BitSet(lines()); + } + noSonarLines.forEach(l -> this.noSonarLines.set(l - 1)); + } + + public boolean hasNoSonarAt(int line) { + if (this.noSonarLines == null) { + return false; + } + return this.noSonarLines.get(line - 1); + } + + public boolean isIgnoreAllIssues() { + return ignoreAllIssues; + } + + public void setIgnoreAllIssues(boolean ignoreAllIssues) { + this.ignoreAllIssues = ignoreAllIssues; + } + + public void addIgnoreIssuesOnLineRanges(Collection<int[]> lineRanges) { + if (this.ignoreIssuesOnlineRanges == null) { + this.ignoreIssuesOnlineRanges = new ArrayList<>(); + } + this.ignoreIssuesOnlineRanges.addAll(lineRanges); + } + + public boolean isIgnoreAllIssuesOnLine(@Nullable Integer line) { + if (line == null || ignoreIssuesOnlineRanges == null) { + return false; + } + return ignoreIssuesOnlineRanges.stream().anyMatch(r -> r[0] <= line && line <= r[1]); + } + + public void setExecutableLines(Set<Integer> executableLines) { + checkState(this.executableLines == null, "Executable lines have already been saved for file: {}", this.toString()); + this.executableLines = new BitSet(lines()); + executableLines.forEach(l -> this.executableLines.set(l - 1)); + } + + public Optional<Set<Integer>> getExecutableLines() { + if (this.executableLines == null) { + return Optional.empty(); + } + return Optional.of(this.executableLines.stream().map(i -> i + 1).boxed().collect(Collectors.toSet())); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultInputModule.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultInputModule.java new file mode 100644 index 00000000000..a729bff7856 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultInputModule.java @@ -0,0 +1,81 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import javax.annotation.CheckForNull; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.batch.bootstrap.ProjectDefinition; +import org.sonar.api.batch.fs.InputModule; +import org.sonar.api.scan.filesystem.PathResolver; + +import static org.sonar.api.config.internal.MultivalueProperty.parseAsCsv; + +@Immutable +public class DefaultInputModule extends AbstractProjectOrModule implements InputModule { + + private final List<Path> sourceDirsOrFiles; + private final List<Path> testDirsOrFiles; + + /** + * For testing only! + */ + public DefaultInputModule(ProjectDefinition definition) { + this(definition, 0); + } + + public DefaultInputModule(ProjectDefinition definition, int scannerComponentId) { + super(definition, scannerComponentId); + + this.sourceDirsOrFiles = initSources(definition, ProjectDefinition.SOURCES_PROPERTY); + this.testDirsOrFiles = initSources(definition, ProjectDefinition.TESTS_PROPERTY); + } + + @CheckForNull + private List<Path> initSources(ProjectDefinition module, String propertyKey) { + if (!module.properties().containsKey(propertyKey)) { + return null; + } + List<Path> result = new ArrayList<>(); + PathResolver pathResolver = new PathResolver(); + String srcPropValue = module.properties().get(propertyKey); + if (srcPropValue != null) { + for (String sourcePath : parseAsCsv(propertyKey, srcPropValue)) { + File dirOrFile = pathResolver.relativeFile(getBaseDir().toFile(), sourcePath); + if (dirOrFile.exists()) { + result.add(dirOrFile.toPath()); + } + } + } + return result; + } + + public Optional<List<Path>> getSourceDirsOrFiles() { + return Optional.ofNullable(sourceDirsOrFiles); + } + + public Optional<List<Path>> getTestDirsOrFiles() { + return Optional.ofNullable(testDirsOrFiles); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultInputProject.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultInputProject.java new file mode 100644 index 00000000000..9dcd0254b00 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultInputProject.java @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs; + +import javax.annotation.concurrent.Immutable; +import org.sonar.api.batch.bootstrap.ProjectDefinition; +import org.sonar.api.scanner.fs.InputProject; + +@Immutable +public class DefaultInputProject extends AbstractProjectOrModule implements InputProject { + + /** + * For testing only! + */ + public DefaultInputProject(ProjectDefinition definition) { + super(definition, 0); + } + + public DefaultInputProject(ProjectDefinition definition, int scannerComponentId) { + super(definition, scannerComponentId); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultTextPointer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultTextPointer.java new file mode 100644 index 00000000000..5cfc84fc712 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultTextPointer.java @@ -0,0 +1,74 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs; + +import org.sonar.api.batch.fs.TextPointer; + +/** + * @since 5.2 + */ +public class DefaultTextPointer implements TextPointer { + + private final int line; + private final int lineOffset; + + public DefaultTextPointer(int line, int lineOffset) { + this.line = line; + this.lineOffset = lineOffset; + } + + @Override + public int line() { + return line; + } + + @Override + public int lineOffset() { + return lineOffset; + } + + @Override + public String toString() { + return "[line=" + line + ", lineOffset=" + lineOffset + "]"; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof DefaultTextPointer)) { + return false; + } + DefaultTextPointer other = (DefaultTextPointer) obj; + return other.line == this.line && other.lineOffset == this.lineOffset; + } + + @Override + public int hashCode() { + return 37 * this.line + lineOffset; + } + + @Override + public int compareTo(TextPointer o) { + if (this.line == o.line()) { + return Integer.compare(this.lineOffset, o.lineOffset()); + } + return Integer.compare(this.line, o.line()); + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultTextRange.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultTextRange.java new file mode 100644 index 00000000000..f83c330e7ea --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/DefaultTextRange.java @@ -0,0 +1,74 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs; + +import org.sonar.api.batch.fs.TextPointer; +import org.sonar.api.batch.fs.TextRange; + +/** + * @since 5.2 + */ +public class DefaultTextRange implements TextRange { + + private final TextPointer start; + private final TextPointer end; + + public DefaultTextRange(TextPointer start, TextPointer end) { + this.start = start; + this.end = end; + } + + @Override + public TextPointer start() { + return start; + } + + @Override + public TextPointer end() { + return end; + } + + @Override + public boolean overlap(TextRange another) { + // [A,B] and [C,D] + // B > C && D > A + return this.end.compareTo(another.start()) > 0 && another.end().compareTo(this.start) > 0; + } + + @Override + public String toString() { + return "Range[from " + start + " to " + end + "]"; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof DefaultTextRange)) { + return false; + } + DefaultTextRange other = (DefaultTextRange) obj; + return start.equals(other.start) && end.equals(other.end); + } + + @Override + public int hashCode() { + return start.hashCode() * 17 + end.hashCode(); + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/FileMetadata.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/FileMetadata.java new file mode 100644 index 00000000000..1b984168d51 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/FileMetadata.java @@ -0,0 +1,162 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.internal.Metadata; +import org.sonar.scanner.fs.charhandler.CharHandler; +import org.sonar.scanner.fs.charhandler.FileHashComputer; +import org.sonar.scanner.fs.charhandler.LineCounter; +import org.sonar.scanner.fs.charhandler.LineHashComputer; +import org.sonar.scanner.fs.charhandler.LineOffsetCounter; + +/** + * Computes hash of files. Ends of Lines are ignored, so files with + * same content but different EOL encoding have the same hash. + */ +@Immutable +public class FileMetadata { + private static final char LINE_FEED = '\n'; + private static final char CARRIAGE_RETURN = '\r'; + + /** + * Compute hash of a file ignoring line ends differences. + * Maximum performance is needed. + */ + public Metadata readMetadata(InputStream stream, Charset encoding, String filePath, @Nullable CharHandler otherHandler) { + LineCounter lineCounter = new LineCounter(filePath, encoding); + FileHashComputer fileHashComputer = new FileHashComputer(filePath); + LineOffsetCounter lineOffsetCounter = new LineOffsetCounter(); + + if (otherHandler != null) { + CharHandler[] handlers = {lineCounter, fileHashComputer, lineOffsetCounter, otherHandler}; + readFile(stream, encoding, filePath, handlers); + } else { + CharHandler[] handlers = {lineCounter, fileHashComputer, lineOffsetCounter}; + readFile(stream, encoding, filePath, handlers); + } + return new Metadata(lineCounter.lines(), lineCounter.nonBlankLines(), fileHashComputer.getHash(), lineOffsetCounter.getOriginalLineStartOffsets(), + lineOffsetCounter.getOriginalLineEndOffsets(), + lineOffsetCounter.getLastValidOffset()); + } + + public Metadata readMetadata(InputStream stream, Charset encoding, String filePath) { + return readMetadata(stream, encoding, filePath, null); + } + + /** + * For testing purpose + */ + public Metadata readMetadata(Reader reader) { + LineCounter lineCounter = new LineCounter("fromString", StandardCharsets.UTF_16); + FileHashComputer fileHashComputer = new FileHashComputer("fromString"); + LineOffsetCounter lineOffsetCounter = new LineOffsetCounter(); + CharHandler[] handlers = {lineCounter, fileHashComputer, lineOffsetCounter}; + + try { + read(reader, handlers); + } catch (IOException e) { + throw new IllegalStateException("Should never occur", e); + } + return new Metadata(lineCounter.lines(), lineCounter.nonBlankLines(), fileHashComputer.getHash(), lineOffsetCounter.getOriginalLineStartOffsets(), + lineOffsetCounter.getOriginalLineEndOffsets(), + lineOffsetCounter.getLastValidOffset()); + } + + public static void readFile(InputStream stream, Charset encoding, String filePath, CharHandler[] handlers) { + try (Reader reader = new BufferedReader(new InputStreamReader(stream, encoding))) { + read(reader, handlers); + } catch (IOException e) { + throw new IllegalStateException(String.format("Fail to read file '%s' with encoding '%s'", filePath, encoding), e); + } + } + + private static void read(Reader reader, CharHandler[] handlers) throws IOException { + char c; + int i = reader.read(); + boolean afterCR = false; + while (i != -1) { + c = (char) i; + if (afterCR) { + for (CharHandler handler : handlers) { + if (c == CARRIAGE_RETURN) { + handler.newLine(); + handler.handleAll(c); + } else if (c == LINE_FEED) { + handler.handleAll(c); + handler.newLine(); + } else { + handler.newLine(); + handler.handleIgnoreEoL(c); + handler.handleAll(c); + } + } + afterCR = c == CARRIAGE_RETURN; + } else if (c == LINE_FEED) { + for (CharHandler handler : handlers) { + handler.handleAll(c); + handler.newLine(); + } + } else if (c == CARRIAGE_RETURN) { + afterCR = true; + for (CharHandler handler : handlers) { + handler.handleAll(c); + } + } else { + for (CharHandler handler : handlers) { + handler.handleIgnoreEoL(c); + handler.handleAll(c); + } + } + i = reader.read(); + } + for (CharHandler handler : handlers) { + if (afterCR) { + handler.newLine(); + } + handler.eof(); + } + } + + @FunctionalInterface + public interface LineHashConsumer { + void consume(int lineIdx, @Nullable byte[] hash); + } + + /** + * Compute a MD5 hash of each line of the file after removing of all blank chars + */ + public static void computeLineHashesForIssueTracking(InputFile f, LineHashConsumer consumer) { + try { + readFile(f.inputStream(), f.charset(), f.absolutePath(), new CharHandler[] {new LineHashComputer(consumer, f.file())}); + } catch (IOException e) { + throw new IllegalStateException("Failed to compute line hashes for " + f.absolutePath(), e); + } + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/InputModuleHierarchy.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/InputModuleHierarchy.java new file mode 100644 index 00000000000..9048824f032 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/InputModuleHierarchy.java @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs; + +import java.util.Collection; +import javax.annotation.CheckForNull; +import javax.annotation.concurrent.Immutable; + +@Immutable +public interface InputModuleHierarchy { + DefaultInputModule root(); + + boolean isRoot(DefaultInputModule module); + + Collection<DefaultInputModule> children(DefaultInputModule module); + + @CheckForNull + DefaultInputModule parent(DefaultInputModule module); + + @CheckForNull + String relativePath(DefaultInputModule module); + + @CheckForNull + String relativePathToRoot(DefaultInputModule module); +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/PathPattern.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/PathPattern.java new file mode 100644 index 00000000000..5d5947d9927 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/PathPattern.java @@ -0,0 +1,136 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs; + +import java.nio.file.Path; +import javax.annotation.concurrent.ThreadSafe; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.utils.PathUtils; +import org.sonar.api.utils.WildcardPattern; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +@ThreadSafe +public abstract class PathPattern { + + private static final Logger LOG = Loggers.get(PathPattern.class); + + /** + * @deprecated since 6.6 + */ + @Deprecated + private static final String ABSOLUTE_PATH_PATTERN_PREFIX = "file:"; + final WildcardPattern pattern; + + PathPattern(String pattern) { + this.pattern = WildcardPattern.create(pattern); + } + + public abstract boolean match(Path absolutePath, Path relativePath); + + public abstract boolean match(Path absolutePath, Path relativePath, boolean caseSensitiveFileExtension); + + public static PathPattern create(String s) { + String trimmed = StringUtils.trim(s); + if (StringUtils.startsWithIgnoreCase(trimmed, ABSOLUTE_PATH_PATTERN_PREFIX)) { + LOG.warn("Using absolute path pattern is deprecated. Please use relative path instead of '" + trimmed + "'"); + return new AbsolutePathPattern(StringUtils.substring(trimmed, ABSOLUTE_PATH_PATTERN_PREFIX.length())); + } + return new RelativePathPattern(trimmed); + } + + public static PathPattern[] create(String[] s) { + PathPattern[] result = new PathPattern[s.length]; + for (int i = 0; i < s.length; i++) { + result[i] = create(s[i]); + } + return result; + } + + /** + * @deprecated since 6.6 + */ + @Deprecated + private static class AbsolutePathPattern extends PathPattern { + private AbsolutePathPattern(String pattern) { + super(pattern); + } + + @Override + public boolean match(Path absolutePath, Path relativePath) { + return match(absolutePath, relativePath, true); + } + + @Override + public boolean match(Path absolutePath, Path relativePath, boolean caseSensitiveFileExtension) { + String path = PathUtils.sanitize(absolutePath.toString()); + if (!caseSensitiveFileExtension) { + String extension = sanitizeExtension(FilenameUtils.getExtension(path)); + if (StringUtils.isNotBlank(extension)) { + path = StringUtils.removeEndIgnoreCase(path, extension); + path = path + extension; + } + } + return pattern.match(path); + } + + @Override + public String toString() { + return ABSOLUTE_PATH_PATTERN_PREFIX + pattern.toString(); + } + } + + /** + * Path relative to module basedir + */ + private static class RelativePathPattern extends PathPattern { + private RelativePathPattern(String pattern) { + super(pattern); + } + + @Override + public boolean match(Path absolutePath, Path relativePath) { + return match(absolutePath, relativePath, true); + } + + @Override + public boolean match(Path absolutePath, Path relativePath, boolean caseSensitiveFileExtension) { + String path = PathUtils.sanitize(relativePath.toString()); + if (!caseSensitiveFileExtension) { + String extension = sanitizeExtension(FilenameUtils.getExtension(path)); + if (StringUtils.isNotBlank(extension)) { + path = StringUtils.removeEndIgnoreCase(path, extension); + path = path + extension; + } + } + return path != null && pattern.match(path); + } + + @Override + public String toString() { + return pattern.toString(); + } + } + + static String sanitizeExtension(String suffix) { + return StringUtils.lowerCase(StringUtils.removeStart(suffix, ".")); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/SensorStrategy.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/SensorStrategy.java new file mode 100644 index 00000000000..244a01b052d --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/SensorStrategy.java @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs; + +import org.sonar.api.batch.fs.InputFile; + +/** + * A shared, mutable object in the project container. + * It's used during the execution of sensors to decide whether + * sensors should be executed once for the entire project, or per-module. + * It is also injected into each InputFile to change the behavior of {@link InputFile#relativePath()} + */ +public class SensorStrategy { + + private boolean global = true; + + public boolean isGlobal() { + return global; + } + + public void setGlobal(boolean global) { + this.global = global; + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/TestInputFileBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/TestInputFileBuilder.java new file mode 100644 index 00000000000..5d6e8b9bc9c --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/TestInputFileBuilder.java @@ -0,0 +1,279 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs; + +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.batch.bootstrap.ProjectDefinition; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.internal.Metadata; +import org.sonar.api.utils.PathUtils; + +/** + * Intended to be used in unit tests that need to create {@link InputFile}s. + * An InputFile is unambiguously identified by a <b>module key</b> and a <b>relative path</b>, so these parameters are mandatory. + * <p> + * A module base directory is only needed to construct absolute paths. + * <p> + * Examples of usage of the constructors: + * + * <pre> + * InputFile file1 = TestInputFileBuilder.create("module1", "myfile.java").build(); + * InputFile file2 = TestInputFileBuilder.create("", fs.baseDir(), myfile).build(); + * </pre> + * <p> + * file1 will have the "module1" as both module key and module base directory. + * file2 has an empty string as module key, and a relative path which is the path from the filesystem base directory to myfile. + * + * @since 6.3 + */ +public class TestInputFileBuilder { + private static int batchId = 1; + + private final int id; + private final String relativePath; + private final String projectKey; + @CheckForNull + private Path projectBaseDir; + private Path moduleBaseDir; + private String language; + private InputFile.Type type = InputFile.Type.MAIN; + private InputFile.Status status; + private int lines = -1; + private Charset charset; + private String hash; + private int nonBlankLines; + private int[] originalLineStartOffsets = new int[0]; + private int[] originalLineEndOffsets = new int[0]; + private int lastValidOffset = -1; + private boolean publish = true; + private String contents; + + /** + * Create a InputFile identified by the given project key and relative path. + */ + public TestInputFileBuilder(String projectKey, String relativePath) { + this(projectKey, relativePath, batchId++); + } + + /** + * Create a InputFile with a given module key and module base directory. + * The relative path is generated comparing the file path to the module base directory. + * filePath must point to a file that is within the module base directory. + */ + public TestInputFileBuilder(String projectKey, File moduleBaseDir, File filePath) { + String relativePath = moduleBaseDir.toPath().relativize(filePath.toPath()).toString(); + this.projectKey = projectKey; + setModuleBaseDir(moduleBaseDir.toPath()); + this.relativePath = PathUtils.sanitize(relativePath); + this.id = batchId++; + } + + public TestInputFileBuilder(String projectKey, String relativePath, int id) { + this.projectKey = projectKey; + setModuleBaseDir(Paths.get(projectKey)); + this.relativePath = PathUtils.sanitize(relativePath); + this.id = id; + } + + public static TestInputFileBuilder create(String moduleKey, File moduleBaseDir, File filePath) { + return new TestInputFileBuilder(moduleKey, moduleBaseDir, filePath); + } + + public static TestInputFileBuilder create(String moduleKey, String relativePath) { + return new TestInputFileBuilder(moduleKey, relativePath); + } + + public static int nextBatchId() { + return batchId++; + } + + public TestInputFileBuilder setProjectBaseDir(Path projectBaseDir) { + this.projectBaseDir = normalize(projectBaseDir); + return this; + } + + public TestInputFileBuilder setModuleBaseDir(Path moduleBaseDir) { + this.moduleBaseDir = normalize(moduleBaseDir); + return this; + } + + private static Path normalize(Path path) { + try { + return path.normalize().toRealPath(LinkOption.NOFOLLOW_LINKS); + } catch (IOException e) { + return path.normalize(); + } + } + + public TestInputFileBuilder setLanguage(@Nullable String language) { + this.language = language; + return this; + } + + public TestInputFileBuilder setType(InputFile.Type type) { + this.type = type; + return this; + } + + public TestInputFileBuilder setStatus(InputFile.Status status) { + this.status = status; + return this; + } + + public TestInputFileBuilder setLines(int lines) { + this.lines = lines; + return this; + } + + public TestInputFileBuilder setCharset(Charset charset) { + this.charset = charset; + return this; + } + + public TestInputFileBuilder setHash(String hash) { + this.hash = hash; + return this; + } + + /** + * Set contents of the file and calculates metadata from it. + * The contents will be returned by {@link InputFile#contents()} and {@link InputFile#inputStream()} and can be + * inconsistent with the actual physical file pointed by {@link InputFile#path()}, {@link InputFile#absolutePath()}, etc. + */ + public TestInputFileBuilder setContents(String content) { + this.contents = content; + initMetadata(content); + return this; + } + + public TestInputFileBuilder setNonBlankLines(int nonBlankLines) { + this.nonBlankLines = nonBlankLines; + return this; + } + + public TestInputFileBuilder setLastValidOffset(int lastValidOffset) { + this.lastValidOffset = lastValidOffset; + return this; + } + + public TestInputFileBuilder setOriginalLineStartOffsets(int[] originalLineStartOffsets) { + this.originalLineStartOffsets = originalLineStartOffsets; + return this; + } + + public TestInputFileBuilder setOriginalLineEndOffsets(int[] originalLineEndOffsets) { + this.originalLineEndOffsets = originalLineEndOffsets; + return this; + } + + public TestInputFileBuilder setPublish(boolean publish) { + this.publish = publish; + return this; + } + + public TestInputFileBuilder setMetadata(Metadata metadata) { + this.setLines(metadata.lines()); + this.setLastValidOffset(metadata.lastValidOffset()); + this.setNonBlankLines(metadata.nonBlankLines()); + this.setHash(metadata.hash()); + this.setOriginalLineStartOffsets(metadata.originalLineStartOffsets()); + this.setOriginalLineEndOffsets(metadata.originalLineEndOffsets()); + return this; + } + + public TestInputFileBuilder initMetadata(String content) { + return setMetadata(new FileMetadata().readMetadata(new StringReader(content))); + } + + public DefaultInputFile build() { + Path absolutePath = moduleBaseDir.resolve(relativePath); + if (projectBaseDir == null) { + projectBaseDir = moduleBaseDir; + } + String projectRelativePath = projectBaseDir.relativize(absolutePath).toString(); + DefaultIndexedFile indexedFile = new DefaultIndexedFile(absolutePath, projectKey, projectRelativePath, relativePath, type, language, id, new SensorStrategy()); + DefaultInputFile inputFile = new DefaultInputFile(indexedFile, + f -> f.setMetadata(new Metadata(lines, nonBlankLines, hash, originalLineStartOffsets, originalLineEndOffsets, lastValidOffset)), + contents); + inputFile.setStatus(status); + inputFile.setCharset(charset); + inputFile.setPublished(publish); + return inputFile; + } + + public static DefaultInputModule newDefaultInputModule(String moduleKey, File baseDir) { + ProjectDefinition definition = ProjectDefinition.create() + .setKey(moduleKey) + .setBaseDir(baseDir) + .setWorkDir(new File(baseDir, ".sonar")); + return newDefaultInputModule(definition); + } + + public static DefaultInputModule newDefaultInputModule(ProjectDefinition projectDefinition) { + return new DefaultInputModule(projectDefinition, TestInputFileBuilder.nextBatchId()); + } + + public static DefaultInputModule newDefaultInputModule(AbstractProjectOrModule parent, String key) throws IOException { + Path basedir = parent.getBaseDir().resolve(key); + Files.createDirectory(basedir); + return newDefaultInputModule(key, basedir.toFile()); + } + + public static DefaultInputProject newDefaultInputProject(String projectKey, File baseDir) { + ProjectDefinition definition = ProjectDefinition.create() + .setKey(projectKey) + .setBaseDir(baseDir) + .setWorkDir(new File(baseDir, ".sonar")); + return newDefaultInputProject(definition); + } + + public static DefaultInputProject newDefaultInputProject(ProjectDefinition projectDefinition) { + return new DefaultInputProject(projectDefinition, TestInputFileBuilder.nextBatchId()); + } + + public static DefaultInputProject newDefaultInputProject(String key, Path baseDir) throws IOException { + Files.createDirectory(baseDir); + return newDefaultInputProject(key, baseDir.toFile()); + } + + public static DefaultInputDir newDefaultInputDir(AbstractProjectOrModule module, String relativePath) throws IOException { + Path basedir = module.getBaseDir().resolve(relativePath); + Files.createDirectory(basedir); + return new DefaultInputDir(module.key(), relativePath) + .setModuleBaseDir(module.getBaseDir()); + } + + public static DefaultInputFile newDefaultInputFile(Path projectBaseDir, AbstractProjectOrModule module, String relativePath) { + return new TestInputFileBuilder(module.key(), relativePath) + .setStatus(InputFile.Status.SAME) + .setProjectBaseDir(projectBaseDir) + .setModuleBaseDir(module.getBaseDir()) + .build(); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/CharHandler.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/CharHandler.java new file mode 100644 index 00000000000..737f6c29071 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/CharHandler.java @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.charhandler; + +public abstract class CharHandler { + + public void handleAll(char c) { + } + + public void handleIgnoreEoL(char c) { + } + + public void newLine() { + } + + public void eof() { + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/FileHashComputer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/FileHashComputer.java new file mode 100644 index 00000000000..46229a5c70b --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/FileHashComputer.java @@ -0,0 +1,81 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.charhandler; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; + +public class FileHashComputer extends CharHandler { + private static final char LINE_FEED = '\n'; + + + private MessageDigest globalMd5Digest = DigestUtils.getMd5Digest(); + private StringBuilder sb = new StringBuilder(); + private final CharsetEncoder encoder; + private final String filePath; + + public FileHashComputer(String filePath) { + encoder = StandardCharsets.UTF_8.newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + this.filePath = filePath; + } + + @Override + public void handleIgnoreEoL(char c) { + sb.append(c); + } + + @Override + public void newLine() { + sb.append(LINE_FEED); + processBuffer(); + sb.setLength(0); + } + + @Override + public void eof() { + if (sb.length() > 0) { + processBuffer(); + } + } + + private void processBuffer() { + try { + if (sb.length() > 0) { + ByteBuffer encoded = encoder.encode(CharBuffer.wrap(sb)); + globalMd5Digest.update(encoded.array(), 0, encoded.limit()); + } + } catch (CharacterCodingException e) { + throw new IllegalStateException("Error encoding line hash in file: " + filePath, e); + } + } + + public String getHash() { + return Hex.encodeHexString(globalMd5Digest.digest()); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/IntArrayList.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/IntArrayList.java new file mode 100644 index 00000000000..12674145399 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/IntArrayList.java @@ -0,0 +1,117 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.charhandler; + +import java.util.Arrays; +import java.util.Collection; + +/** + * Specialization of {@link java.util.ArrayList} to create a list of int (only append elements) and then produce an int[]. + */ +class IntArrayList { + + /** + * Default initial capacity. + */ + private static final int DEFAULT_CAPACITY = 10; + + /** + * Shared empty array instance used for default sized empty instances. We + * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when + * first element is added. + */ + private static final int[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; + + /** + * The array buffer into which the elements of the ArrayList are stored. + * The capacity of the IntArrayList is the length of this array buffer. Any + * empty IntArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA + * will be expanded to DEFAULT_CAPACITY when the first element is added. + */ + private int[] elementData; + + /** + * The size of the IntArrayList (the number of elements it contains). + */ + private int size; + + /** + * Constructs an empty list with an initial capacity of ten. + */ + public IntArrayList() { + this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; + } + + /** + * Trims the capacity of this <tt>IntArrayList</tt> instance to be the + * list's current size and return the internal array. An application can use this operation to minimize + * the storage of an <tt>IntArrayList</tt> instance. + */ + public int[] trimAndGet() { + if (size < elementData.length) { + elementData = Arrays.copyOf(elementData, size); + } + return elementData; + } + + private void ensureCapacityInternal(int minCapacity) { + int capacity = minCapacity; + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + capacity = Math.max(DEFAULT_CAPACITY, minCapacity); + } + + ensureExplicitCapacity(capacity); + } + + private void ensureExplicitCapacity(int minCapacity) { + if (minCapacity - elementData.length > 0) { + grow(minCapacity); + } + } + + /** + * Increases the capacity to ensure that it can hold at least the + * number of elements specified by the minimum capacity argument. + * + * @param minCapacity the desired minimum capacity + */ + private void grow(int minCapacity) { + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + (oldCapacity >> 1); + if (newCapacity - minCapacity < 0) { + newCapacity = minCapacity; + } + elementData = Arrays.copyOf(elementData, newCapacity); + } + + /** + * Appends the specified element to the end of this list. + * + * @param e element to be appended to this list + * @return <tt>true</tt> (as specified by {@link Collection#add}) + */ + public boolean add(int e) { + ensureCapacityInternal(size + 1); + elementData[size] = e; + size++; + return true; + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/LineCounter.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/LineCounter.java new file mode 100644 index 00000000000..7cac0f2b0a0 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/LineCounter.java @@ -0,0 +1,82 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.charhandler; + +import java.nio.charset.Charset; +import org.sonar.api.CoreProperties; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +public class LineCounter extends CharHandler { + private static final Logger LOG = Loggers.get(LineCounter.class); + + private int lines = 1; + private int nonBlankLines = 0; + private boolean blankLine = true; + boolean alreadyLoggedInvalidCharacter = false; + private final String filePath; + private final Charset encoding; + + public LineCounter(String filePath, Charset encoding) { + this.filePath = filePath; + this.encoding = encoding; + } + + @Override + public void handleAll(char c) { + if (!alreadyLoggedInvalidCharacter && c == '\ufffd') { + LOG.warn("Invalid character encountered in file {} at line {} for encoding {}. Please fix file content or configure the encoding to be used using property '{}'.", filePath, + lines, encoding, CoreProperties.ENCODING_PROPERTY); + alreadyLoggedInvalidCharacter = true; + } + } + + @Override + public void newLine() { + lines++; + if (!blankLine) { + nonBlankLines++; + } + blankLine = true; + } + + @Override + public void handleIgnoreEoL(char c) { + if (!Character.isWhitespace(c)) { + blankLine = false; + } + } + + @Override + public void eof() { + if (!blankLine) { + nonBlankLines++; + } + } + + public int lines() { + return lines; + } + + public int nonBlankLines() { + return nonBlankLines; + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/LineHashComputer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/LineHashComputer.java new file mode 100644 index 00000000000..5435499e505 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/LineHashComputer.java @@ -0,0 +1,81 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.charhandler; + +import java.io.File; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import org.apache.commons.codec.digest.DigestUtils; +import org.sonar.scanner.fs.FileMetadata; + +public class LineHashComputer extends CharHandler { + private final MessageDigest lineMd5Digest = DigestUtils.getMd5Digest(); + private final CharsetEncoder encoder; + private final StringBuilder sb = new StringBuilder(); + private final FileMetadata.LineHashConsumer consumer; + private final File file; + private int line = 1; + + public LineHashComputer(FileMetadata.LineHashConsumer consumer, File f) { + this.consumer = consumer; + this.file = f; + this.encoder = StandardCharsets.UTF_8.newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + } + + @Override + public void handleIgnoreEoL(char c) { + if (!Character.isWhitespace(c)) { + sb.append(c); + } + } + + @Override + public void newLine() { + processBuffer(); + sb.setLength(0); + line++; + } + + @Override + public void eof() { + if (this.line > 0) { + processBuffer(); + } + } + + private void processBuffer() { + try { + if (sb.length() > 0) { + ByteBuffer encoded = encoder.encode(CharBuffer.wrap(sb)); + lineMd5Digest.update(encoded.array(), 0, encoded.limit()); + consumer.consume(line, lineMd5Digest.digest()); + } + } catch (CharacterCodingException e) { + throw new IllegalStateException("Error encoding line hash in file: " + file.getAbsolutePath(), e); + } + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/LineOffsetCounter.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/LineOffsetCounter.java new file mode 100644 index 00000000000..74b7f4867ba --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/LineOffsetCounter.java @@ -0,0 +1,74 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.charhandler; + +public class LineOffsetCounter extends CharHandler { + private long currentOriginalLineStartOffset = 0; + private long currentOriginalLineEndOffset = 0; + private final IntArrayList originalLineStartOffsets = new IntArrayList(); + private final IntArrayList originalLineEndOffsets = new IntArrayList(); + private long lastValidOffset = 0; + + public LineOffsetCounter() { + originalLineStartOffsets.add(0); + } + + @Override + public void handleAll(char c) { + currentOriginalLineStartOffset++; + } + + @Override + public void handleIgnoreEoL(char c) { + currentOriginalLineEndOffset++; + } + + @Override + public void newLine() { + if (currentOriginalLineStartOffset > Integer.MAX_VALUE) { + throw new IllegalStateException("File is too big: " + currentOriginalLineStartOffset); + } + originalLineStartOffsets.add((int) currentOriginalLineStartOffset); + originalLineEndOffsets.add((int) currentOriginalLineEndOffset); + currentOriginalLineEndOffset = currentOriginalLineStartOffset; + } + + @Override + public void eof() { + originalLineEndOffsets.add((int) currentOriginalLineEndOffset); + lastValidOffset = currentOriginalLineStartOffset; + } + + public int[] getOriginalLineStartOffsets() { + return originalLineStartOffsets.trimAndGet(); + } + + public int[] getOriginalLineEndOffsets() { + return originalLineEndOffsets.trimAndGet(); + } + + public int getLastValidOffset() { + if (lastValidOffset > Integer.MAX_VALUE) { + throw new IllegalStateException("File is too big: " + lastValidOffset); + } + return (int) lastValidOffset; + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/package-info.java new file mode 100644 index 00000000000..4e5375fc17f --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/charhandler/package-info.java @@ -0,0 +1,24 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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. + */ +@ParametersAreNonnullByDefault +package org.sonar.scanner.fs.charhandler; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/package-info.java new file mode 100644 index 00000000000..e971997aa86 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/package-info.java @@ -0,0 +1,24 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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. + */ +@ParametersAreNonnullByDefault +package org.sonar.scanner.fs; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/AbsolutePathPredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/AbsolutePathPredicate.java new file mode 100644 index 00000000000..c55099a8884 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/AbsolutePathPredicate.java @@ -0,0 +1,63 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import java.io.File; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import org.sonar.api.batch.fs.FileSystem.Index; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.scan.filesystem.PathResolver; +import org.sonar.api.utils.PathUtils; + +/** + * @since 4.2 + */ +class AbsolutePathPredicate extends AbstractFilePredicate { + + private final String path; + private final Path baseDir; + + AbsolutePathPredicate(String path, Path baseDir) { + this.baseDir = baseDir; + this.path = PathUtils.sanitize(path); + } + + @Override + public boolean apply(InputFile f) { + return path.equals(f.absolutePath()); + } + + @Override + public Iterable<InputFile> get(Index index) { + String relative = PathUtils.sanitize(new PathResolver().relativePath(baseDir.toFile(), new File(path))); + if (relative == null) { + return Collections.emptyList(); + } + InputFile f = index.inputFile(relative); + return f != null ? Arrays.asList(f) : Collections.<InputFile>emptyList(); + } + + @Override + public int priority() { + return USE_INDEX; + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/AbstractFilePredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/AbstractFilePredicate.java new file mode 100644 index 00000000000..4f6952611d7 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/AbstractFilePredicate.java @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import java.util.stream.StreamSupport; +import org.sonar.api.batch.fs.FileSystem.Index; +import org.sonar.api.batch.fs.InputFile; + +/** + * Partial implementation of {@link OptimizedFilePredicate}. + * @since 5.1 + */ +public abstract class AbstractFilePredicate implements OptimizedFilePredicate { + + protected static final int DEFAULT_PRIORITY = 10; + protected static final int USE_INDEX = 20; + + @Override + public Iterable<InputFile> filter(Iterable<InputFile> target) { + return () -> StreamSupport.stream(target.spliterator(), false) + .filter(this::apply) + .iterator(); + } + + @Override + public Iterable<InputFile> get(Index index) { + return filter(index.inputFiles()); + } + + @Override + public int priority() { + return DEFAULT_PRIORITY; + } + + @Override + public final int compareTo(OptimizedFilePredicate o) { + return o.priority() - priority(); + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/AndPredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/AndPredicate.java new file mode 100644 index 00000000000..024964256ba --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/AndPredicate.java @@ -0,0 +1,103 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.FileSystem.Index; +import org.sonar.api.batch.fs.InputFile; + +import static java.util.stream.Collectors.toList; + +/** + * @since 4.2 + */ +class AndPredicate extends AbstractFilePredicate implements OperatorPredicate { + + private final List<OptimizedFilePredicate> predicates = new ArrayList<>(); + + private AndPredicate() { + } + + public static FilePredicate create(Collection<FilePredicate> predicates) { + if (predicates.isEmpty()) { + return TruePredicate.TRUE; + } + AndPredicate result = new AndPredicate(); + for (FilePredicate filePredicate : predicates) { + if (filePredicate == TruePredicate.TRUE) { + continue; + } else if (filePredicate == FalsePredicate.FALSE) { + return FalsePredicate.FALSE; + } else if (filePredicate instanceof AndPredicate) { + result.predicates.addAll(((AndPredicate) filePredicate).predicates); + } else { + result.predicates.add(OptimizedFilePredicateAdapter.create(filePredicate)); + } + } + Collections.sort(result.predicates); + return result; + } + + @Override + public boolean apply(InputFile f) { + for (OptimizedFilePredicate predicate : predicates) { + if (!predicate.apply(f)) { + return false; + } + } + return true; + } + + @Override + public Iterable<InputFile> filter(Iterable<InputFile> target) { + Iterable<InputFile> result = target; + for (OptimizedFilePredicate predicate : predicates) { + result = predicate.filter(result); + } + return result; + } + + @Override + public Iterable<InputFile> get(Index index) { + if (predicates.isEmpty()) { + return index.inputFiles(); + } + // Optimization, use get on first predicate then filter with next predicates + Iterable<InputFile> result = predicates.get(0).get(index); + for (int i = 1; i < predicates.size(); i++) { + result = predicates.get(i).filter(result); + } + return result; + } + + Collection<OptimizedFilePredicate> predicates() { + return predicates; + } + + @Override + public List<FilePredicate> operands() { + return predicates.stream().map(p -> (FilePredicate) p).collect(toList()); + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/DefaultFilePredicates.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/DefaultFilePredicates.java new file mode 100644 index 00000000000..60bb37e9896 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/DefaultFilePredicates.java @@ -0,0 +1,214 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import java.io.File; +import java.net.URI; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.FilePredicates; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.InputFile.Status; +import org.sonar.scanner.fs.PathPattern; + +/** + * Factory of {@link FilePredicate} + * + * @since 4.2 + */ +public class DefaultFilePredicates implements FilePredicates { + + private final Path baseDir; + + /** + * Client code should use {@link org.sonar.api.batch.fs.FileSystem#predicates()} to get an instance + */ + public DefaultFilePredicates(Path baseDir) { + this.baseDir = baseDir; + } + + /** + * Returns a predicate that always evaluates to true + */ + @Override + public FilePredicate all() { + return TruePredicate.TRUE; + } + + /** + * Returns a predicate that always evaluates to false + */ + @Override + public FilePredicate none() { + return FalsePredicate.FALSE; + } + + @Override + public FilePredicate hasAbsolutePath(String s) { + return new AbsolutePathPredicate(s, baseDir); + } + + /** + * non-normalized path and Windows-style path are supported + */ + @Override + public FilePredicate hasRelativePath(String s) { + return new RelativePathPredicate(s); + } + + @Override + public FilePredicate hasFilename(String s) { + return new FilenamePredicate(s); + } + + @Override + public FilePredicate hasExtension(String s) { + return new FileExtensionPredicate(s); + } + + @Override + public FilePredicate hasURI(URI uri) { + return new URIPredicate(uri, baseDir); + } + + @Override + public FilePredicate matchesPathPattern(String inclusionPattern) { + return new PathPatternPredicate(PathPattern.create(inclusionPattern)); + } + + @Override + public FilePredicate matchesPathPatterns(String[] inclusionPatterns) { + if (inclusionPatterns.length == 0) { + return TruePredicate.TRUE; + } + FilePredicate[] predicates = new FilePredicate[inclusionPatterns.length]; + for (int i = 0; i < inclusionPatterns.length; i++) { + predicates[i] = new PathPatternPredicate(PathPattern.create(inclusionPatterns[i])); + } + return or(predicates); + } + + @Override + public FilePredicate doesNotMatchPathPattern(String exclusionPattern) { + return not(matchesPathPattern(exclusionPattern)); + } + + @Override + public FilePredicate doesNotMatchPathPatterns(String[] exclusionPatterns) { + if (exclusionPatterns.length == 0) { + return TruePredicate.TRUE; + } + return not(matchesPathPatterns(exclusionPatterns)); + } + + @Override + public FilePredicate hasPath(String s) { + File file = new File(s); + if (file.isAbsolute()) { + return hasAbsolutePath(s); + } + return hasRelativePath(s); + } + + @Override + public FilePredicate is(File ioFile) { + if (ioFile.isAbsolute()) { + return hasAbsolutePath(ioFile.getAbsolutePath()); + } + return hasRelativePath(ioFile.getPath()); + } + + @Override + public FilePredicate hasLanguage(String language) { + return new LanguagePredicate(language); + } + + @Override + public FilePredicate hasLanguages(Collection<String> languages) { + List<FilePredicate> list = new ArrayList<>(); + for (String language : languages) { + list.add(hasLanguage(language)); + } + return or(list); + } + + @Override + public FilePredicate hasLanguages(String... languages) { + List<FilePredicate> list = new ArrayList<>(); + for (String language : languages) { + list.add(hasLanguage(language)); + } + return or(list); + } + + @Override + public FilePredicate hasType(InputFile.Type type) { + return new TypePredicate(type); + } + + @Override + public FilePredicate not(FilePredicate p) { + return new NotPredicate(p); + } + + @Override + public FilePredicate or(Collection<FilePredicate> or) { + return OrPredicate.create(or); + } + + @Override + public FilePredicate or(FilePredicate... or) { + return OrPredicate.create(Arrays.asList(or)); + } + + @Override + public FilePredicate or(FilePredicate first, FilePredicate second) { + return OrPredicate.create(Arrays.asList(first, second)); + } + + @Override + public FilePredicate and(Collection<FilePredicate> and) { + return AndPredicate.create(and); + } + + @Override + public FilePredicate and(FilePredicate... and) { + return AndPredicate.create(Arrays.asList(and)); + } + + @Override + public FilePredicate and(FilePredicate first, FilePredicate second) { + return AndPredicate.create(Arrays.asList(first, second)); + } + + @Override + public FilePredicate hasStatus(Status status) { + return new StatusPredicate(status); + } + + @Override + public FilePredicate hasAnyStatus() { + return new StatusPredicate(null); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/FalsePredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/FalsePredicate.java new file mode 100644 index 00000000000..e76bb84e6ae --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/FalsePredicate.java @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import java.util.Collections; +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.FileSystem.Index; +import org.sonar.api.batch.fs.InputFile; + +class FalsePredicate extends AbstractFilePredicate { + + static final FilePredicate FALSE = new FalsePredicate(); + + @Override + public boolean apply(InputFile inputFile) { + return false; + } + + @Override + public Iterable<InputFile> filter(Iterable<InputFile> target) { + return Collections.emptyList(); + } + + @Override + public Iterable<InputFile> get(Index index) { + return Collections.emptyList(); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/FileExtensionPredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/FileExtensionPredicate.java new file mode 100644 index 00000000000..46f257f25a4 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/FileExtensionPredicate.java @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import java.util.Locale; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; + +/** + * @since 6.3 + */ +public class FileExtensionPredicate extends AbstractFilePredicate { + + private final String extension; + + public FileExtensionPredicate(String extension) { + this.extension = lowercase(extension); + } + + @Override + public boolean apply(InputFile inputFile) { + return extension.equals(getExtension(inputFile)); + } + + @Override + public Iterable<InputFile> get(FileSystem.Index index) { + return index.getFilesByExtension(extension); + } + + public static String getExtension(InputFile inputFile) { + return getExtension(inputFile.filename()); + } + + static String getExtension(String name) { + int index = name.lastIndexOf('.'); + if (index < 0) { + return ""; + } + return lowercase(name.substring(index + 1)); + } + + private static String lowercase(String extension) { + return extension.toLowerCase(Locale.ENGLISH); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/FilenamePredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/FilenamePredicate.java new file mode 100644 index 00000000000..a99d9314c77 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/FilenamePredicate.java @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; + +/** + * @since 6.3 + */ +public class FilenamePredicate extends AbstractFilePredicate { + private final String filename; + + public FilenamePredicate(String filename) { + this.filename = filename; + } + + @Override + public boolean apply(InputFile inputFile) { + return filename.equals(inputFile.filename()); + } + + @Override + public Iterable<InputFile> get(FileSystem.Index index) { + return index.getFilesByName(filename); + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/LanguagePredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/LanguagePredicate.java new file mode 100644 index 00000000000..5492e224558 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/LanguagePredicate.java @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import org.sonar.api.batch.fs.InputFile; + +/** + * @since 4.2 + */ +class LanguagePredicate extends AbstractFilePredicate { + private final String language; + + LanguagePredicate(String language) { + this.language = language; + } + + @Override + public boolean apply(InputFile f) { + return language.equals(f.language()); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/NotPredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/NotPredicate.java new file mode 100644 index 00000000000..5bc68adfbe7 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/NotPredicate.java @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import java.util.Arrays; +import java.util.List; +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.scanner.fs.predicates.AbstractFilePredicate; +import org.sonar.scanner.fs.predicates.OperatorPredicate; + +/** + * @since 4.2 + */ +class NotPredicate extends AbstractFilePredicate implements OperatorPredicate { + + private final FilePredicate predicate; + + NotPredicate(FilePredicate predicate) { + this.predicate = predicate; + } + + @Override + public boolean apply(InputFile f) { + return !predicate.apply(f); + } + + @Override + public List<FilePredicate> operands() { + return Arrays.asList(predicate); + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/OperatorPredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/OperatorPredicate.java new file mode 100644 index 00000000000..c4493091865 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/OperatorPredicate.java @@ -0,0 +1,32 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import java.util.List; +import org.sonar.api.batch.fs.FilePredicate; + +/** + * A predicate that associate other predicates + */ +public interface OperatorPredicate extends FilePredicate { + + List<FilePredicate> operands(); + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/OptimizedFilePredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/OptimizedFilePredicate.java new file mode 100644 index 00000000000..9b5216d5549 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/OptimizedFilePredicate.java @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; + +/** + * Optimized version of FilePredicate allowing to speed up query by looking at InputFile by index. + */ +public interface OptimizedFilePredicate extends FilePredicate, Comparable<OptimizedFilePredicate> { + + /** + * Filter provided files to keep only the ones that are valid for this predicate + */ + Iterable<InputFile> filter(Iterable<InputFile> inputFiles); + + /** + * Get all files that are valid for this predicate. + */ + Iterable<InputFile> get(FileSystem.Index index); + + /** + * For optimization. FilePredicates will be applied in priority order. For example when doing + * p.and(p1, p2, p3) then p1, p2 and p3 will be applied according to their priority value. Higher priority value + * are applied first. + * Assign a high priority when the predicate will likely highly reduce the set of InputFiles to filter. Also + * {@link RelativePathPredicate} and AbsolutePathPredicate have a high priority since they are using cache index. + */ + int priority(); +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/OptimizedFilePredicateAdapter.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/OptimizedFilePredicateAdapter.java new file mode 100644 index 00000000000..ba46aff6847 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/OptimizedFilePredicateAdapter.java @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.InputFile; + +public class OptimizedFilePredicateAdapter extends AbstractFilePredicate { + + private FilePredicate unoptimizedPredicate; + + private OptimizedFilePredicateAdapter(FilePredicate unoptimizedPredicate) { + this.unoptimizedPredicate = unoptimizedPredicate; + } + + @Override + public boolean apply(InputFile inputFile) { + return unoptimizedPredicate.apply(inputFile); + } + + public static OptimizedFilePredicate create(FilePredicate predicate) { + if (predicate instanceof OptimizedFilePredicate) { + return (OptimizedFilePredicate) predicate; + } else { + return new OptimizedFilePredicateAdapter(predicate); + } + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/OrPredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/OrPredicate.java new file mode 100644 index 00000000000..2690a1261d9 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/OrPredicate.java @@ -0,0 +1,76 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.InputFile; + +/** + * @since 4.2 + */ +class OrPredicate extends AbstractFilePredicate implements OperatorPredicate { + + private final List<FilePredicate> predicates = new ArrayList<>(); + + private OrPredicate() { + } + + public static FilePredicate create(Collection<FilePredicate> predicates) { + if (predicates.isEmpty()) { + return TruePredicate.TRUE; + } + OrPredicate result = new OrPredicate(); + for (FilePredicate filePredicate : predicates) { + if (filePredicate == TruePredicate.TRUE) { + return TruePredicate.TRUE; + } else if (filePredicate == FalsePredicate.FALSE) { + continue; + } else if (filePredicate instanceof OrPredicate) { + result.predicates.addAll(((OrPredicate) filePredicate).predicates); + } else { + result.predicates.add(filePredicate); + } + } + return result; + } + + @Override + public boolean apply(InputFile f) { + for (FilePredicate predicate : predicates) { + if (predicate.apply(f)) { + return true; + } + } + return false; + } + + Collection<FilePredicate> predicates() { + return predicates; + } + + @Override + public List<FilePredicate> operands() { + return predicates; + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/PathPatternPredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/PathPatternPredicate.java new file mode 100644 index 00000000000..0fdabcbcd31 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/PathPatternPredicate.java @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import java.nio.file.Paths; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.scanner.fs.PathPattern; + +/** + * @since 4.2 + */ +class PathPatternPredicate extends AbstractFilePredicate { + + private final PathPattern pattern; + + PathPatternPredicate(PathPattern pattern) { + this.pattern = pattern; + } + + @Override + public boolean apply(InputFile f) { + return pattern.match(f.path(), Paths.get(f.relativePath())); + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/RelativePathPredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/RelativePathPredicate.java new file mode 100644 index 00000000000..d79221ea7f3 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/RelativePathPredicate.java @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import java.util.Collections; +import javax.annotation.Nullable; +import org.sonar.api.batch.fs.FileSystem.Index; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.utils.PathUtils; + +/** + * @since 4.2 + */ +public class RelativePathPredicate extends AbstractFilePredicate { + + @Nullable + private final String path; + + RelativePathPredicate(String path) { + this.path = PathUtils.sanitize(path); + } + + public String path() { + return path; + } + + @Override + public boolean apply(InputFile f) { + if (path == null) { + return false; + } + + return path.equals(f.relativePath()); + } + + @Override + public Iterable<InputFile> get(Index index) { + if (path != null) { + InputFile f = index.inputFile(this.path); + if (f != null) { + return Collections.singletonList(f); + } + } + return Collections.emptyList(); + } + + @Override + public int priority() { + return USE_INDEX; + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/StatusPredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/StatusPredicate.java new file mode 100644 index 00000000000..fb88b965a07 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/StatusPredicate.java @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import javax.annotation.Nullable; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.scanner.fs.predicates.AbstractFilePredicate; + +/** + * @deprecated since 7.8 + */ +@Deprecated +public class StatusPredicate extends AbstractFilePredicate { + + private final InputFile.Status status; + + StatusPredicate(@Nullable InputFile.Status status) { + this.status = status; + } + + @Override + public boolean apply(InputFile f) { + return status == null || status == f.status(); + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/TruePredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/TruePredicate.java new file mode 100644 index 00000000000..562515ce06e --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/TruePredicate.java @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.FileSystem.Index; +import org.sonar.api.batch.fs.InputFile; + +class TruePredicate extends AbstractFilePredicate { + + static final FilePredicate TRUE = new TruePredicate(); + + @Override + public boolean apply(InputFile inputFile) { + return true; + } + + @Override + public Iterable<InputFile> get(Index index) { + return index.inputFiles(); + } + + @Override + public Iterable<InputFile> filter(Iterable<InputFile> target) { + return target; + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/TypePredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/TypePredicate.java new file mode 100644 index 00000000000..3cddc59ad17 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/TypePredicate.java @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import org.sonar.api.batch.fs.InputFile; + +/** + * @since 4.2 + */ +class TypePredicate extends AbstractFilePredicate { + + private final InputFile.Type type; + + TypePredicate(InputFile.Type type) { + this.type = type; + } + + @Override + public boolean apply(InputFile f) { + return type == f.type(); + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/URIPredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/URIPredicate.java new file mode 100644 index 00000000000..9e25267de05 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/fs/predicates/URIPredicate.java @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.fs.predicates; + +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import org.sonar.api.batch.fs.FileSystem.Index; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.scan.filesystem.PathResolver; + +/** + * @since 6.6 + */ +class URIPredicate extends AbstractFilePredicate { + + private final URI uri; + private final Path baseDir; + + URIPredicate(URI uri, Path baseDir) { + this.baseDir = baseDir; + this.uri = uri; + } + + @Override + public boolean apply(InputFile f) { + return uri.equals(f.uri()); + } + + @Override + public Iterable<InputFile> get(Index index) { + Path path = Paths.get(uri); + Optional<String> relative = PathResolver.relativize(baseDir, path); + if (!relative.isPresent()) { + return Collections.emptyList(); + } + InputFile f = index.inputFile(relative.get()); + return f != null ? Arrays.asList(f) : Collections.<InputFile>emptyList(); + } + + @Override + public int priority() { + return USE_INDEX; + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/AbstractDefaultIssue.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/AbstractDefaultIssue.java new file mode 100644 index 00000000000..113d44544f7 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/AbstractDefaultIssue.java @@ -0,0 +1,122 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.issue; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import javax.annotation.Nullable; +import org.sonar.api.batch.fs.InputComponent; +import org.sonar.api.batch.fs.internal.DefaultInputDir; +import org.sonar.api.batch.fs.internal.DefaultInputModule; +import org.sonar.api.batch.fs.internal.DefaultInputProject; +import org.sonar.api.batch.sensor.internal.DefaultStorable; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.batch.sensor.issue.Issue.Flow; +import org.sonar.api.batch.sensor.issue.IssueLocation; +import org.sonar.api.batch.sensor.issue.NewIssueLocation; +import org.sonar.api.utils.PathUtils; + +import static java.util.Collections.unmodifiableList; +import static java.util.stream.Collectors.toList; +import static org.sonar.api.utils.Preconditions.checkArgument; +import static org.sonar.api.utils.Preconditions.checkState; + +public abstract class AbstractDefaultIssue<T extends AbstractDefaultIssue> extends DefaultStorable { + protected IssueLocation primaryLocation; + protected List<List<IssueLocation>> flows = new ArrayList<>(); + protected DefaultInputProject project; + + protected AbstractDefaultIssue(DefaultInputProject project) { + this(project, null); + } + + public AbstractDefaultIssue(DefaultInputProject project, @Nullable SensorStorage storage) { + super(storage); + this.project = project; + } + + public IssueLocation primaryLocation() { + return primaryLocation; + } + + public List<Flow> flows() { + return this.flows.stream() + .<Flow>map(l -> () -> unmodifiableList(new ArrayList<>(l))) + .collect(toList()); + } + + public NewIssueLocation newLocation() { + return new DefaultIssueLocation(); + } + + public T at(NewIssueLocation primaryLocation) { + checkArgument(primaryLocation != null, "Cannot use a location that is null"); + checkState(this.primaryLocation == null, "at() already called"); + this.primaryLocation = rewriteLocation((DefaultIssueLocation) primaryLocation); + checkArgument(this.primaryLocation.inputComponent() != null, "Cannot use a location with no input component"); + return (T) this; + } + + public T addLocation(NewIssueLocation secondaryLocation) { + flows.add(Collections.singletonList(rewriteLocation((DefaultIssueLocation) secondaryLocation))); + return (T) this; + } + + public T addFlow(Iterable<NewIssueLocation> locations) { + List<IssueLocation> flowAsList = new ArrayList<>(); + for (NewIssueLocation issueLocation : locations) { + flowAsList.add(rewriteLocation((DefaultIssueLocation) issueLocation)); + } + flows.add(flowAsList); + return (T) this; + } + + private DefaultIssueLocation rewriteLocation(DefaultIssueLocation location) { + InputComponent component = location.inputComponent(); + Optional<Path> dirOrModulePath = Optional.empty(); + + if (component instanceof DefaultInputDir) { + DefaultInputDir dirComponent = (DefaultInputDir) component; + dirOrModulePath = Optional.of(project.getBaseDir().relativize(dirComponent.path())); + } else if (component instanceof DefaultInputModule && !Objects.equals(project.key(), component.key())) { + DefaultInputModule moduleComponent = (DefaultInputModule) component; + dirOrModulePath = Optional.of(project.getBaseDir().relativize(moduleComponent.getBaseDir())); + } + + if (dirOrModulePath.isPresent()) { + String path = PathUtils.sanitize(dirOrModulePath.get().toString()); + DefaultIssueLocation fixedLocation = new DefaultIssueLocation(); + fixedLocation.on(project); + StringBuilder fullMessage = new StringBuilder(); + if (path != null && !path.isEmpty()) { + fullMessage.append("[").append(path).append("] "); + } + fullMessage.append(location.message()); + fixedLocation.message(fullMessage.toString()); + return fixedLocation; + } else { + return location; + } + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DefaultIssue.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DefaultIssue.java new file mode 100644 index 00000000000..bf36bf5ac55 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DefaultIssue.java @@ -0,0 +1,93 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.issue; + +import javax.annotation.Nullable; +import org.sonar.api.batch.fs.internal.DefaultInputProject; +import org.sonar.api.batch.rule.Severity; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.batch.sensor.issue.Issue; +import org.sonar.api.batch.sensor.issue.IssueLocation; +import org.sonar.api.batch.sensor.issue.NewIssue; +import org.sonar.api.rule.RuleKey; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static org.sonar.api.utils.Preconditions.checkArgument; +import static org.sonar.api.utils.Preconditions.checkState; + +public class DefaultIssue extends AbstractDefaultIssue<DefaultIssue> implements Issue, NewIssue { + private RuleKey ruleKey; + private Double gap; + private Severity overriddenSeverity; + + public DefaultIssue(DefaultInputProject project) { + this(project, null); + } + + public DefaultIssue(DefaultInputProject project, @Nullable SensorStorage storage) { + super(project, storage); + } + + public DefaultIssue forRule(RuleKey ruleKey) { + this.ruleKey = ruleKey; + return this; + } + + public RuleKey ruleKey() { + return this.ruleKey; + } + + @Override + public DefaultIssue gap(@Nullable Double gap) { + checkArgument(gap == null || gap >= 0, format("Gap must be greater than or equal 0 (got %s)", gap)); + this.gap = gap; + return this; + } + + @Override + public DefaultIssue overrideSeverity(@Nullable Severity severity) { + this.overriddenSeverity = severity; + return this; + } + + @Override + public Severity overriddenSeverity() { + return this.overriddenSeverity; + } + + @Override + public Double gap() { + return this.gap; + } + + @Override + public IssueLocation primaryLocation() { + return primaryLocation; + } + + @Override + public void doSave() { + requireNonNull(this.ruleKey, "ruleKey is mandatory on issue"); + checkState(primaryLocation != null, "Primary location is mandatory on every issue"); + storage.store(this); + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DefaultIssueLocation.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DefaultIssueLocation.java new file mode 100644 index 00000000000..95147bc75a0 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DefaultIssueLocation.java @@ -0,0 +1,92 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.issue; + +import javax.annotation.Nullable; +import org.sonar.api.batch.fs.InputComponent; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.sensor.issue.IssueLocation; +import org.sonar.api.batch.sensor.issue.NewIssueLocation; + +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang.StringUtils.abbreviate; +import static org.apache.commons.lang.StringUtils.trim; +import static org.sonar.api.utils.Preconditions.checkArgument; +import static org.sonar.api.utils.Preconditions.checkState; + +public class DefaultIssueLocation implements NewIssueLocation, IssueLocation { + + private InputComponent component; + private TextRange textRange; + private String message; + + @Override + public DefaultIssueLocation on(InputComponent component) { + checkArgument(component != null, "Component can't be null"); + checkState(this.component == null, "on() already called"); + this.component = component; + return this; + } + + @Override + public DefaultIssueLocation at(TextRange location) { + checkState(this.component != null, "at() should be called after on()"); + checkState(this.component.isFile(), "at() should be called only for an InputFile."); + DefaultInputFile file = (DefaultInputFile) this.component; + file.validate(location); + this.textRange = location; + return this; + } + + @Override + public DefaultIssueLocation message(String message) { + requireNonNull(message, "Message can't be null"); + if (message.contains("\u0000")) { + throw new IllegalArgumentException(unsupportedCharacterError(message, component)); + } + this.message = abbreviate(trim(message), MESSAGE_MAX_SIZE); + return this; + } + + private static String unsupportedCharacterError(String message, @Nullable InputComponent component) { + String error = "Character \\u0000 is not supported in issue message '" + message + "'"; + if (component != null) { + error += ", on component: " + component.toString(); + } + return error; + } + + @Override + public InputComponent inputComponent() { + return this.component; + } + + @Override + public TextRange textRange() { + return textRange; + } + + @Override + public String message() { + return this.message; + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ignore/scanner/IssueExclusionsLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ignore/scanner/IssueExclusionsLoader.java index 6e639385b09..858a4dd5d2f 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ignore/scanner/IssueExclusionsLoader.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ignore/scanner/IssueExclusionsLoader.java @@ -24,10 +24,10 @@ import java.util.List; import javax.annotation.CheckForNull; import org.apache.commons.lang.StringUtils; import org.sonar.api.batch.fs.internal.DefaultInputFile; -import org.sonar.api.batch.fs.internal.charhandler.CharHandler; import org.sonar.api.notifications.AnalysisWarnings; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; +import org.sonar.scanner.fs.charhandler.CharHandler; import org.sonar.scanner.issue.ignore.IgnoreIssuesFilter; import org.sonar.scanner.issue.ignore.pattern.BlockIssuePattern; import org.sonar.scanner.issue.ignore.pattern.IssueExclusionPatternInitializer; diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ignore/scanner/IssueExclusionsRegexpScanner.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ignore/scanner/IssueExclusionsRegexpScanner.java index a08ba55a593..19079dbac4f 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ignore/scanner/IssueExclusionsRegexpScanner.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ignore/scanner/IssueExclusionsRegexpScanner.java @@ -26,9 +26,9 @@ import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.sonar.api.batch.fs.internal.DefaultInputFile; -import org.sonar.api.batch.fs.internal.charhandler.CharHandler; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; +import org.sonar.scanner.fs.charhandler.CharHandler; import org.sonar.scanner.issue.ignore.pattern.LineRange; import org.sonar.scanner.issue.ignore.scanner.IssueExclusionsLoader.DoubleRegexpMatcher; diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/DefaultRules.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/DefaultRules.java new file mode 100644 index 00000000000..def001211e7 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/DefaultRules.java @@ -0,0 +1,89 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.api.batch.rule.internal; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.batch.rule.Rule; +import org.sonar.api.batch.rule.Rules; +import org.sonar.api.rule.RuleKey; + +@Immutable +class DefaultRules implements Rules { + private final Map<String, List<Rule>> rulesByRepository; + private final Map<String, Map<String, List<Rule>>> rulesByRepositoryAndInternalKey; + private final Map<RuleKey, Rule> rulesByRuleKey; + + DefaultRules(Collection<NewRule> newRules) { + Map<String, List<Rule>> rulesByRepositoryBuilder = new HashMap<>(); + Map<String, Map<String, List<Rule>>> rulesByRepositoryAndInternalKeyBuilder = new HashMap<>(); + Map<RuleKey, Rule> rulesByRuleKeyBuilder = new HashMap<>(); + + for (NewRule newRule : newRules) { + DefaultRule r = new DefaultRule(newRule); + rulesByRuleKeyBuilder.put(r.key(), r); + rulesByRepositoryBuilder.computeIfAbsent(r.key().repository(), x -> new ArrayList<>()).add(r); + addToTable(rulesByRepositoryAndInternalKeyBuilder, r); + } + + rulesByRuleKey = Collections.unmodifiableMap(rulesByRuleKeyBuilder); + rulesByRepository = Collections.unmodifiableMap(rulesByRepositoryBuilder); + rulesByRepositoryAndInternalKey = Collections.unmodifiableMap(rulesByRepositoryAndInternalKeyBuilder); + } + + private static void addToTable(Map<String, Map<String, List<Rule>>> rulesByRepositoryAndInternalKeyBuilder, DefaultRule r) { + if (r.internalKey() == null) { + return; + } + + rulesByRepositoryAndInternalKeyBuilder + .computeIfAbsent(r.key().repository(), x -> new HashMap<>()) + .computeIfAbsent(r.internalKey(), x -> new ArrayList<>()) + .add(r); + } + + @Override + public Rule find(RuleKey ruleKey) { + return rulesByRuleKey.get(ruleKey); + } + + @Override + public Collection<Rule> findAll() { + return rulesByRepository.values().stream().flatMap(List::stream).collect(Collectors.toList()); + } + + @Override + public Collection<Rule> findByRepository(String repository) { + return rulesByRepository.getOrDefault(repository, Collections.emptyList()); + } + + @Override + public Collection<Rule> findByInternalKey(String repository, String internalKey) { + return rulesByRepositoryAndInternalKey + .getOrDefault(repository, Collections.emptyMap()) + .getOrDefault(internalKey, Collections.emptyList()); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/RulesBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/RulesBuilder.java new file mode 100644 index 00000000000..f7ebe363d09 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/RulesBuilder.java @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.api.batch.rule.internal; + +import org.sonar.api.batch.rule.Rules; +import org.sonar.api.rule.RuleKey; + +import java.util.HashMap; +import java.util.Map; + +/** + * For unit testing and internal use only. + * + * @since 4.2 + */ + +public class RulesBuilder { + + private final Map<RuleKey, NewRule> map = new HashMap<>(); + + public NewRule add(RuleKey key) { + if (map.containsKey(key)) { + throw new IllegalStateException(String.format("Rule '%s' already exists", key)); + } + NewRule newRule = new NewRule(key); + map.put(key, newRule); + return newRule; + } + + public Rules build() { + return new DefaultRules(map.values()); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java index 71ab90450b8..411625c7546 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java @@ -24,7 +24,6 @@ import javax.annotation.Nullable; import org.sonar.api.SonarEdition; import org.sonar.api.SonarRuntime; import org.sonar.api.batch.fs.internal.DefaultInputModule; -import org.sonar.api.batch.fs.internal.FileMetadata; import org.sonar.api.batch.fs.internal.InputModuleHierarchy; import org.sonar.api.batch.fs.internal.SensorStrategy; import org.sonar.api.batch.rule.CheckFactory; @@ -67,6 +66,7 @@ import org.sonar.scanner.cpd.CpdSettings; import org.sonar.scanner.cpd.index.SonarCpdBlockIndex; import org.sonar.scanner.deprecated.test.TestPlanBuilder; import org.sonar.scanner.deprecated.test.TestableBuilder; +import org.sonar.scanner.fs.FileMetadata; import org.sonar.scanner.issue.IssueFilters; import org.sonar.scanner.issue.IssuePublisher; import org.sonar.scanner.issue.ignore.EnforceIssuesFilter; diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/AdditionalFilePredicates.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/AdditionalFilePredicates.java index cdc9c74f499..6c1bb65332a 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/AdditionalFilePredicates.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/AdditionalFilePredicates.java @@ -20,7 +20,7 @@ package org.sonar.scanner.scan.filesystem; import org.sonar.api.batch.fs.InputFile; -import org.sonar.api.batch.fs.internal.AbstractFilePredicate; +import org.sonar.scanner.fs.predicates.AbstractFilePredicate; /** * Additional {@link org.sonar.api.batch.fs.FilePredicate}s that are diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/DefaultModuleFileSystem.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/DefaultModuleFileSystem.java index dd13c11c1b3..74e82cd8535 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/DefaultModuleFileSystem.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/DefaultModuleFileSystem.java @@ -20,8 +20,8 @@ package org.sonar.scanner.scan.filesystem; import com.google.common.annotations.VisibleForTesting; -import org.sonar.api.batch.fs.internal.DefaultFileSystem; import org.sonar.api.batch.fs.internal.DefaultInputModule; +import org.sonar.scanner.fs.DefaultFileSystem; public class DefaultModuleFileSystem extends DefaultFileSystem { diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/DefaultProjectFileSystem.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/DefaultProjectFileSystem.java index f81ccc42432..7e0dbc6f6af 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/DefaultProjectFileSystem.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/DefaultProjectFileSystem.java @@ -20,8 +20,8 @@ package org.sonar.scanner.scan.filesystem; import com.google.common.annotations.VisibleForTesting; -import org.sonar.api.batch.fs.internal.DefaultFileSystem; import org.sonar.api.batch.fs.internal.DefaultInputProject; +import org.sonar.scanner.fs.DefaultFileSystem; public class DefaultProjectFileSystem extends DefaultFileSystem { diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/InputComponentStore.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/InputComponentStore.java index e4cb0672f7a..cd9f0db7347 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/InputComponentStore.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/InputComponentStore.java @@ -35,10 +35,10 @@ import java.util.stream.Stream; import javax.annotation.CheckForNull; import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.batch.fs.InputFile; -import org.sonar.api.batch.fs.internal.DefaultFileSystem; import org.sonar.api.batch.fs.internal.DefaultInputFile; import org.sonar.api.batch.fs.internal.DefaultInputModule; -import org.sonar.api.batch.fs.internal.FileExtensionPredicate; +import org.sonar.scanner.fs.DefaultFileSystem; +import org.sonar.scanner.fs.predicates.FileExtensionPredicate; import org.sonar.scanner.scan.branch.BranchConfiguration; /** diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/MetadataGenerator.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/MetadataGenerator.java index cf7833e18fc..80731033ac0 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/MetadataGenerator.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/MetadataGenerator.java @@ -24,10 +24,10 @@ import java.io.InputStream; import java.nio.charset.Charset; import org.sonar.api.batch.fs.InputFile.Type; import org.sonar.api.batch.fs.internal.DefaultInputFile; -import org.sonar.api.batch.fs.internal.FileMetadata; import org.sonar.api.batch.fs.internal.Metadata; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; +import org.sonar.scanner.fs.FileMetadata; import org.sonar.scanner.issue.ignore.scanner.IssueExclusionsLoader; public class MetadataGenerator { diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ModuleInputComponentStore.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ModuleInputComponentStore.java index c9d1d3c115e..e6d52849c63 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ModuleInputComponentStore.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ModuleInputComponentStore.java @@ -23,8 +23,8 @@ import java.util.SortedSet; import org.sonar.api.batch.ScannerSide; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.InputModule; -import org.sonar.api.batch.fs.internal.DefaultFileSystem; import org.sonar.api.batch.fs.internal.SensorStrategy; +import org.sonar.scanner.fs.DefaultFileSystem; @ScannerSide public class ModuleInputComponentStore extends DefaultFileSystem.Cache { diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/InMemorySensorStorage.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/InMemorySensorStorage.java new file mode 100644 index 00000000000..c0ec11759ed --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/InMemorySensorStorage.java @@ -0,0 +1,143 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.sensor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.sonar.api.batch.sensor.code.internal.DefaultSignificantCode; +import org.sonar.api.batch.sensor.coverage.internal.DefaultCoverage; +import org.sonar.api.batch.sensor.cpd.internal.DefaultCpdTokens; +import org.sonar.api.batch.sensor.error.AnalysisError; +import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.batch.sensor.issue.ExternalIssue; +import org.sonar.api.batch.sensor.issue.Issue; +import org.sonar.api.batch.sensor.issue.internal.DefaultExternalIssue; +import org.sonar.api.batch.sensor.measure.Measure; +import org.sonar.api.batch.sensor.rule.AdHocRule; +import org.sonar.api.batch.sensor.rule.internal.DefaultAdHocRule; +import org.sonar.api.batch.sensor.symbol.internal.DefaultSymbolTable; + +import static org.sonar.api.utils.Preconditions.checkArgument; + +class InMemorySensorStorage implements SensorStorage { + + Map<String, Map<String, Measure>> measuresByComponentAndMetric = new HashMap<>(); + + Collection<Issue> allIssues = new ArrayList<>(); + Collection<ExternalIssue> allExternalIssues = new ArrayList<>(); + Collection<AdHocRule> allAdHocRules = new ArrayList<>(); + Collection<AnalysisError> allAnalysisErrors = new ArrayList<>(); + + Map<String, DefaultHighlighting> highlightingByComponent = new HashMap<>(); + Map<String, DefaultCpdTokens> cpdTokensByComponent = new HashMap<>(); + Map<String, List<DefaultCoverage>> coverageByComponent = new HashMap<>(); + Map<String, DefaultSymbolTable> symbolsPerComponent = new HashMap<>(); + Map<String, String> contextProperties = new HashMap<>(); + Map<String, DefaultSignificantCode> significantCodePerComponent = new HashMap<>(); + + @Override + public void store(Measure measure) { + // Emulate duplicate measure check + String componentKey = measure.inputComponent().key(); + String metricKey = measure.metric().key(); + if (measuresByComponentAndMetric.getOrDefault(componentKey, Collections.emptyMap()).containsKey(metricKey)) { + throw new IllegalStateException("Can not add the same measure twice"); + } + measuresByComponentAndMetric.computeIfAbsent(componentKey, x -> new HashMap<>()).put(metricKey, measure); + } + + @Override + public void store(Issue issue) { + allIssues.add(issue); + } + + @Override + public void store(DefaultAdHocRule adHocRule) { + allAdHocRules.add(adHocRule); + } + + @Override + public void store(DefaultHighlighting highlighting) { + String fileKey = highlighting.inputFile().key(); + // Emulate duplicate storage check + if (highlightingByComponent.containsKey(fileKey)) { + throw new UnsupportedOperationException("Trying to save highlighting twice for the same file is not supported: " + highlighting.inputFile()); + } + highlightingByComponent.put(fileKey, highlighting); + } + + @Override + public void store(DefaultCoverage defaultCoverage) { + String fileKey = defaultCoverage.inputFile().key(); + coverageByComponent.computeIfAbsent(fileKey, x -> new ArrayList<>()).add(defaultCoverage); + } + + @Override + public void store(DefaultCpdTokens defaultCpdTokens) { + String fileKey = defaultCpdTokens.inputFile().key(); + // Emulate duplicate storage check + if (cpdTokensByComponent.containsKey(fileKey)) { + throw new UnsupportedOperationException("Trying to save CPD tokens twice for the same file is not supported: " + defaultCpdTokens.inputFile()); + } + cpdTokensByComponent.put(fileKey, defaultCpdTokens); + } + + @Override + public void store(DefaultSymbolTable symbolTable) { + String fileKey = symbolTable.inputFile().key(); + // Emulate duplicate storage check + if (symbolsPerComponent.containsKey(fileKey)) { + throw new UnsupportedOperationException("Trying to save symbol table twice for the same file is not supported: " + symbolTable.inputFile()); + } + symbolsPerComponent.put(fileKey, symbolTable); + } + + @Override + public void store(AnalysisError analysisError) { + allAnalysisErrors.add(analysisError); + } + + @Override + public void storeProperty(String key, String value) { + checkArgument(key != null, "Key of context property must not be null"); + checkArgument(value != null, "Value of context property must not be null"); + contextProperties.put(key, value); + } + + @Override + public void store(DefaultExternalIssue issue) { + allExternalIssues.add(issue); + } + + @Override + public void store(DefaultSignificantCode significantCode) { + String fileKey = significantCode.inputFile().key(); + // Emulate duplicate storage check + if (significantCodePerComponent.containsKey(fileKey)) { + throw new UnsupportedOperationException("Trying to save significant code information twice for the same file is not supported: " + significantCode.inputFile()); + } + significantCodePerComponent.put(fileKey, significantCode); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/SensorContextTester.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/SensorContextTester.java new file mode 100644 index 00000000000..6e81544e146 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/SensorContextTester.java @@ -0,0 +1,409 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.sensor; + +import java.io.File; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.SonarQubeSide; +import org.sonar.api.SonarRuntime; +import org.sonar.api.batch.bootstrap.ProjectDefinition; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.InputModule; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.fs.internal.DefaultInputModule; +import org.sonar.api.batch.fs.internal.DefaultInputProject; +import org.sonar.api.batch.fs.internal.DefaultTextPointer; +import org.sonar.api.batch.rule.ActiveRules; +import org.sonar.api.batch.rule.internal.ActiveRulesBuilder; +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.code.NewSignificantCode; +import org.sonar.api.batch.sensor.code.internal.DefaultSignificantCode; +import org.sonar.api.batch.sensor.coverage.NewCoverage; +import org.sonar.api.batch.sensor.coverage.internal.DefaultCoverage; +import org.sonar.api.batch.sensor.cpd.NewCpdTokens; +import org.sonar.api.batch.sensor.cpd.internal.DefaultCpdTokens; +import org.sonar.api.batch.sensor.cpd.internal.TokensLine; +import org.sonar.api.batch.sensor.error.AnalysisError; +import org.sonar.api.batch.sensor.error.NewAnalysisError; +import org.sonar.api.batch.sensor.error.internal.DefaultAnalysisError; +import org.sonar.api.batch.sensor.highlighting.NewHighlighting; +import org.sonar.api.batch.sensor.highlighting.TypeOfText; +import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting; +import org.sonar.api.batch.sensor.highlighting.internal.SyntaxHighlightingRule; +import org.sonar.api.batch.sensor.issue.ExternalIssue; +import org.sonar.api.batch.sensor.issue.Issue; +import org.sonar.api.batch.sensor.issue.NewExternalIssue; +import org.sonar.api.batch.sensor.issue.NewIssue; +import org.sonar.api.batch.sensor.issue.internal.DefaultExternalIssue; +import org.sonar.api.batch.sensor.issue.internal.DefaultIssue; +import org.sonar.api.batch.sensor.measure.Measure; +import org.sonar.api.batch.sensor.measure.NewMeasure; +import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure; +import org.sonar.api.batch.sensor.rule.AdHocRule; +import org.sonar.api.batch.sensor.rule.NewAdHocRule; +import org.sonar.api.batch.sensor.rule.internal.DefaultAdHocRule; +import org.sonar.api.batch.sensor.symbol.NewSymbolTable; +import org.sonar.api.batch.sensor.symbol.internal.DefaultSymbolTable; +import org.sonar.api.config.Configuration; +import org.sonar.api.config.Settings; +import org.sonar.api.config.internal.ConfigurationBridge; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.internal.MetadataLoader; +import org.sonar.api.internal.SonarRuntimeImpl; +import org.sonar.api.measures.Metric; +import org.sonar.api.scanner.fs.InputProject; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.Version; +import org.sonar.scanner.fs.DefaultFileSystem; + +import static java.util.Collections.unmodifiableMap; + +/** + * Utility class to help testing {@link Sensor}. This is not an API and method signature may evolve. + * <p> + * Usage: call {@link #create(File)} to create an "in memory" implementation of {@link SensorContext} with a filesystem initialized with provided baseDir. + * <p> + * You have to manually register inputFiles using: + * <pre> + * sensorContextTester.fileSystem().add(new DefaultInputFile("myProjectKey", "src/Foo.java") + * .setLanguage("java") + * .initMetadata("public class Foo {\n}")); + * </pre> + * <p> + * Then pass it to your {@link Sensor}. You can then query elements provided by your sensor using methods {@link #allIssues()}, ... + */ +public class SensorContextTester implements SensorContext { + + private Settings settings; + private DefaultFileSystem fs; + private ActiveRules activeRules; + private InMemorySensorStorage sensorStorage; + private DefaultInputProject project; + private DefaultInputModule module; + private SonarRuntime runtime; + private boolean cancelled; + + private SensorContextTester(Path moduleBaseDir) { + this.settings = new MapSettings(); + this.fs = new DefaultFileSystem(moduleBaseDir).setEncoding(Charset.defaultCharset()); + this.activeRules = new ActiveRulesBuilder().build(); + this.sensorStorage = new InMemorySensorStorage(); + this.project = new DefaultInputProject(ProjectDefinition.create().setKey("projectKey").setBaseDir(moduleBaseDir.toFile()).setWorkDir(moduleBaseDir.resolve(".sonar").toFile())); + this.module = new DefaultInputModule(ProjectDefinition.create().setKey("projectKey").setBaseDir(moduleBaseDir.toFile()).setWorkDir(moduleBaseDir.resolve(".sonar").toFile())); + this.runtime = SonarRuntimeImpl.forSonarQube(MetadataLoader.loadVersion(System2.INSTANCE), SonarQubeSide.SCANNER, MetadataLoader.loadEdition(System2.INSTANCE)); + } + + public static SensorContextTester create(File moduleBaseDir) { + return new SensorContextTester(moduleBaseDir.toPath()); + } + + public static SensorContextTester create(Path moduleBaseDir) { + return new SensorContextTester(moduleBaseDir); + } + + @Override + public Settings settings() { + return settings; + } + + @Override + public Configuration config() { + return new ConfigurationBridge(settings); + } + + public SensorContextTester setSettings(Settings settings) { + this.settings = settings; + return this; + } + + @Override + public DefaultFileSystem fileSystem() { + return fs; + } + + public SensorContextTester setFileSystem(DefaultFileSystem fs) { + this.fs = fs; + return this; + } + + @Override + public ActiveRules activeRules() { + return activeRules; + } + + public SensorContextTester setActiveRules(ActiveRules activeRules) { + this.activeRules = activeRules; + return this; + } + + /** + * Default value is the version of this API at compilation time. You can override it + * using {@link #setRuntime(SonarRuntime)} to test your Sensor behaviour. + */ + @Override + public Version getSonarQubeVersion() { + return runtime().getApiVersion(); + } + + /** + * @see #setRuntime(SonarRuntime) to override defaults (SonarQube scanner with version + * of this API as used at compilation time). + */ + @Override + public SonarRuntime runtime() { + return runtime; + } + + public SensorContextTester setRuntime(SonarRuntime runtime) { + this.runtime = runtime; + return this; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + @Override + public InputModule module() { + return module; + } + + @Override + public InputProject project() { + return project; + } + + @Override + public <G extends Serializable> NewMeasure<G> newMeasure() { + return new DefaultMeasure<>(sensorStorage); + } + + public Collection<Measure> measures(String componentKey) { + return sensorStorage.measuresByComponentAndMetric.getOrDefault(componentKey, Collections.emptyMap()).values(); + } + + public <G extends Serializable> Measure<G> measure(String componentKey, Metric<G> metric) { + return measure(componentKey, metric.key()); + } + + public <G extends Serializable> Measure<G> measure(String componentKey, String metricKey) { + return sensorStorage.measuresByComponentAndMetric.getOrDefault(componentKey, Collections.emptyMap()).get(metricKey); + } + + @Override + public NewIssue newIssue() { + return new DefaultIssue(project, sensorStorage); + } + + public Collection<Issue> allIssues() { + return sensorStorage.allIssues; + } + + @Override + public NewExternalIssue newExternalIssue() { + return new DefaultExternalIssue(project, sensorStorage); + } + + @Override + public NewAdHocRule newAdHocRule() { + return new DefaultAdHocRule(sensorStorage); + } + + public Collection<ExternalIssue> allExternalIssues() { + return sensorStorage.allExternalIssues; + } + + public Collection<AdHocRule> allAdHocRules() { + return sensorStorage.allAdHocRules; + } + + public Collection<AnalysisError> allAnalysisErrors() { + return sensorStorage.allAnalysisErrors; + } + + @CheckForNull + public Integer lineHits(String fileKey, int line) { + return sensorStorage.coverageByComponent.getOrDefault(fileKey, Collections.emptyList()).stream() + .map(c -> c.hitsByLine().get(line)) + .flatMap(Stream::of) + .filter(Objects::nonNull) + .reduce(null, SensorContextTester::sumOrNull); + } + + @CheckForNull + public static Integer sumOrNull(@Nullable Integer o1, @Nullable Integer o2) { + return o1 == null ? o2 : (o1 + o2); + } + + @CheckForNull + public Integer conditions(String fileKey, int line) { + return sensorStorage.coverageByComponent.getOrDefault(fileKey, Collections.emptyList()).stream() + .map(c -> c.conditionsByLine().get(line)) + .flatMap(Stream::of) + .filter(Objects::nonNull) + .reduce(null, SensorContextTester::maxOrNull); + } + + @CheckForNull + public Integer coveredConditions(String fileKey, int line) { + return sensorStorage.coverageByComponent.getOrDefault(fileKey, Collections.emptyList()).stream() + .map(c -> c.coveredConditionsByLine().get(line)) + .flatMap(Stream::of) + .filter(Objects::nonNull) + .reduce(null, SensorContextTester::maxOrNull); + } + + @CheckForNull + public TextRange significantCodeTextRange(String fileKey, int line) { + if (sensorStorage.significantCodePerComponent.containsKey(fileKey)) { + return sensorStorage.significantCodePerComponent.get(fileKey) + .significantCodePerLine() + .get(line); + } + return null; + + } + + @CheckForNull + public static Integer maxOrNull(@Nullable Integer o1, @Nullable Integer o2) { + return o1 == null ? o2 : Math.max(o1, o2); + } + + @CheckForNull + public List<TokensLine> cpdTokens(String componentKey) { + DefaultCpdTokens defaultCpdTokens = sensorStorage.cpdTokensByComponent.get(componentKey); + return defaultCpdTokens != null ? defaultCpdTokens.getTokenLines() : null; + } + + @Override + public NewHighlighting newHighlighting() { + return new DefaultHighlighting(sensorStorage); + } + + @Override + public NewCoverage newCoverage() { + return new DefaultCoverage(sensorStorage); + } + + @Override + public NewCpdTokens newCpdTokens() { + return new DefaultCpdTokens(sensorStorage); + } + + @Override + public NewSymbolTable newSymbolTable() { + return new DefaultSymbolTable(sensorStorage); + } + + @Override + public NewAnalysisError newAnalysisError() { + return new DefaultAnalysisError(sensorStorage); + } + + /** + * Return list of syntax highlighting applied for a given position in a file. The result is a list because in theory you + * can apply several styles to the same range. + * + * @param componentKey Key of the file like 'myProjectKey:src/foo.php' + * @param line Line you want to query + * @param lineOffset Offset you want to query. + * @return List of styles applied to this position or empty list if there is no highlighting at this position. + */ + public List<TypeOfText> highlightingTypeAt(String componentKey, int line, int lineOffset) { + DefaultHighlighting syntaxHighlightingData = sensorStorage.highlightingByComponent.get(componentKey); + if (syntaxHighlightingData == null) { + return Collections.emptyList(); + } + List<TypeOfText> result = new ArrayList<>(); + DefaultTextPointer location = new DefaultTextPointer(line, lineOffset); + for (SyntaxHighlightingRule sortedRule : syntaxHighlightingData.getSyntaxHighlightingRuleSet()) { + if (sortedRule.range().start().compareTo(location) <= 0 && sortedRule.range().end().compareTo(location) > 0) { + result.add(sortedRule.getTextType()); + } + } + return result; + } + + /** + * Return list of symbol references ranges for the symbol at a given position in a file. + * + * @param componentKey Key of the file like 'myProjectKey:src/foo.php' + * @param line Line you want to query + * @param lineOffset Offset you want to query. + * @return List of references for the symbol (potentially empty) or null if there is no symbol at this position. + */ + @CheckForNull + public Collection<TextRange> referencesForSymbolAt(String componentKey, int line, int lineOffset) { + DefaultSymbolTable symbolTable = sensorStorage.symbolsPerComponent.get(componentKey); + if (symbolTable == null) { + return null; + } + DefaultTextPointer location = new DefaultTextPointer(line, lineOffset); + for (Map.Entry<TextRange, Set<TextRange>> symbol : symbolTable.getReferencesBySymbol().entrySet()) { + if (symbol.getKey().start().compareTo(location) <= 0 && symbol.getKey().end().compareTo(location) > 0) { + return symbol.getValue(); + } + } + return null; + } + + @Override + public void addContextProperty(String key, String value) { + sensorStorage.storeProperty(key, value); + } + + /** + * @return an immutable map of the context properties defined with {@link SensorContext#addContextProperty(String, String)}. + * @since 6.1 + */ + public Map<String, String> getContextProperties() { + return unmodifiableMap(sensorStorage.contextProperties); + } + + @Override + public void markForPublishing(InputFile inputFile) { + DefaultInputFile file = (DefaultInputFile) inputFile; + file.setPublished(true); + } + + @Override + public NewSignificantCode newSignificantCode() { + return new DefaultSignificantCode(sensorStorage); + } +} |