diff options
author | Duarte Meneses <duarte.meneses@sonarsource.com> | 2019-06-19 13:56:51 -0500 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-07-12 20:21:14 +0200 |
commit | 93dc9770902dc7e168869d88b5ad731bfc0bedd9 (patch) | |
tree | 97ba885661d5cd9a2115fe212df31bacec9f9947 /sonar-plugin-api-impl | |
parent | 7c7d9b6b90244d2c974207862071caccdb2c9bb5 (diff) | |
download | sonarqube-93dc9770902dc7e168869d88b5ad731bfc0bedd9.tar.gz sonarqube-93dc9770902dc7e168869d88b5ad731bfc0bedd9.zip |
Extract implementation from plugin API and create new module sonar-plugin-api-impl
Diffstat (limited to 'sonar-plugin-api-impl')
143 files changed, 15380 insertions, 0 deletions
diff --git a/sonar-plugin-api-impl/build.gradle b/sonar-plugin-api-impl/build.gradle new file mode 100644 index 00000000000..6046e6eef9a --- /dev/null +++ b/sonar-plugin-api-impl/build.gradle @@ -0,0 +1,87 @@ +sonarqube { + properties { + property 'sonar.projectName', "${projectTitle} :: Plugin API Implementation" + } +} + +apply plugin: 'com.github.johnrengelman.shadow' + +dependencies { + // please keep the list grouped by configuration and ordered by name + + compile 'commons-codec:commons-codec' + compile 'commons-io:commons-io' + compile 'commons-lang:commons-lang' + compile 'com.google.code.gson:gson' + compile 'org.apache.commons:commons-csv' + + // shaded, but not relocated + compile project(':sonar-check-api') + compile project(':sonar-plugin-api') + compile project(':sonar-ws') + + + shadow 'org.codehaus.staxmate:staxmate' + shadow 'org.codehaus.woodstox:stax2-api' + shadow 'org.codehaus.woodstox:woodstox-core-lgpl' + + compileOnly 'ch.qos.logback:logback-classic' + compileOnly 'ch.qos.logback:logback-core' + compileOnly 'com.google.code.findbugs:jsr305' + compileOnly 'javax.servlet:javax.servlet-api' + compileOnly 'junit:junit' + compileOnly 'org.slf4j:slf4j-api' + + testCompile 'com.google.guava:guava' + testCompile 'com.tngtech.java:junit-dataprovider' + testCompile 'org.assertj:assertj-core' + testCompile 'org.mockito:mockito-core' + testCompile project(':sonar-scanner-engine') + testCompile project(':server:sonar-server') + +} + +sourceSets { + // Make the compileOnly dependencies available when compiling/running tests + test.compileClasspath += configurations.compileOnly + configurations.shadow + test.runtimeClasspath += configurations.compileOnly + configurations.shadow +} + +def on3Digits(version) { + def projectversion3digits = version - ~/-\w+/ + projectversion3digits = projectversion3digits.tokenize('.').plus(0).take(3).join('.') +} + +import org.apache.tools.ant.filters.ReplaceTokens +processResources { + filter ReplaceTokens, tokens: [ + // The build version is composed of 4 fields, including the semantic version and the build number provided by Travis. + 'project.buildVersion': project.version.endsWith('SNAPSHOT') ? project.version : on3Digits(project.version) + '.' + System.getProperty("buildNumber"), + 'project.version.3digits': project.version.endsWith('SNAPSHOT') ? project.version : on3Digits(project.version) + ] +} + +shadowJar { + configurations = [project.configurations.default] + relocate('com.google', 'org.sonar.api.internal.google') + relocate('org.apache.commons', 'org.sonar.api.internal.apachecommons') + dependencies { + exclude(dependency('org.codehaus.woodstox:woodstox-core-lgpl')) + exclude(dependency('org.codehaus.woodstox:stax2-api')) + exclude(dependency('org.codehaus.staxmate:staxmate')) + } +} + +artifactoryPublish.skip = false + +publishing { + publications { + mavenJava(MavenPublication) { + artifact source: shadowJar, classifier: null + if (release) { + artifact sourcesJar + artifact javadocJar + } + } + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultActiveRule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultActiveRule.java new file mode 100644 index 00000000000..a46bf1a2e82 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultActiveRule.java @@ -0,0 +1,100 @@ +/* + * 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; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.rule.RuleKey; + +@Immutable +public class DefaultActiveRule implements ActiveRule { + private final RuleKey ruleKey; + private final String severity; + private final String internalKey; + private final String language; + private final String templateRuleKey; + private final Map<String, String> params; + private final long createdAt; + private final long updatedAt; + private final String qProfileKey; + + public DefaultActiveRule(NewActiveRule newActiveRule) { + this.severity = newActiveRule.severity; + this.internalKey = newActiveRule.internalKey; + this.templateRuleKey = newActiveRule.templateRuleKey; + this.ruleKey = newActiveRule.ruleKey; + this.params = Collections.unmodifiableMap(new HashMap<>(newActiveRule.params)); + this.language = newActiveRule.language; + this.createdAt = newActiveRule.createdAt; + this.updatedAt = newActiveRule.updatedAt; + this.qProfileKey = newActiveRule.qProfileKey; + } + + @Override + public RuleKey ruleKey() { + return ruleKey; + } + + @Override + public String severity() { + return severity; + } + + @Override + public String language() { + return language; + } + + @Override + public String param(String key) { + return params.get(key); + } + + @Override + public Map<String, String> params() { + // already immutable + return params; + } + + @Override + public String internalKey() { + return internalKey; + } + + @Override + public String templateRuleKey() { + return templateRuleKey; + } + + public long createdAt() { + return createdAt; + } + + public long updatedAt() { + return updatedAt; + } + + @Override + public String qpKey() { + return qProfileKey; + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultRule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultRule.java new file mode 100644 index 00000000000..b8938529024 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultRule.java @@ -0,0 +1,110 @@ +/* + * 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; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.CheckForNull; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rule.RuleStatus; + +@Immutable +public class DefaultRule implements Rule { + + private final RuleKey key; + private final Integer id; + private final String name; + private final String severity; + private final String type; + private final String description; + private final String internalKey; + private final RuleStatus status; + private final Map<String, RuleParam> params; + + public DefaultRule(NewRule newRule) { + this.key = newRule.key; + this.id = newRule.id; + this.name = newRule.name; + this.severity = newRule.severity; + this.type = newRule.type; + this.description = newRule.description; + this.internalKey = newRule.internalKey; + this.status = newRule.status; + + Map<String, RuleParam> builder = new HashMap<>(); + for (NewRuleParam newRuleParam : newRule.params.values()) { + builder.put(newRuleParam.key, new DefaultRuleParam(newRuleParam)); + } + params = Collections.unmodifiableMap(builder); + } + + @Override + public RuleKey key() { + return key; + } + + @CheckForNull + public Integer id() { + return id; + } + + @Override + public String name() { + return name; + } + + @Override + public String severity() { + return severity; + } + + @CheckForNull + public String type() { + return type; + } + + @Override + public String description() { + return description; + } + + @Override + public String internalKey() { + return internalKey; + } + + @Override + public RuleStatus status() { + return status; + } + + @Override + public RuleParam param(String paramKey) { + return params.get(paramKey); + } + + @Override + public Collection<RuleParam> params() { + return params.values(); + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultRuleParam.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultRuleParam.java new file mode 100644 index 00000000000..eb714dcfeda --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultRuleParam.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.api.batch.rule; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +class DefaultRuleParam implements RuleParam { + + private final String key; + private final String description; + + DefaultRuleParam(NewRuleParam p) { + this.key = p.key; + this.description = p.description; + } + + @Override + public String key() { + return key; + } + + @Override + @Nullable + public String description() { + return description; + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/ConfigurationBridge.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/ConfigurationBridge.java new file mode 100644 index 00000000000..fea9800f3a8 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/ConfigurationBridge.java @@ -0,0 +1,52 @@ +/* + * 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.impl.config; + +import java.util.Optional; +import org.sonar.api.config.Settings; +import org.sonar.api.config.Configuration; + +/** + * Used to help migration from {@link Settings} to {@link Configuration} + */ +public class ConfigurationBridge implements Configuration { + + private final Settings settings; + + public ConfigurationBridge(Settings settings) { + this.settings = settings; + } + + @Override + public Optional<String> get(String key) { + return Optional.ofNullable(settings.getString(key)); + } + + @Override + public boolean hasKey(String key) { + return settings.hasKey(key); + } + + @Override + public String[] getStringArray(String key) { + return settings.getStringArray(key); + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/MapSettings.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/MapSettings.java new file mode 100644 index 00000000000..61ac6013c3f --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/MapSettings.java @@ -0,0 +1,112 @@ +/* + * 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.impl.config; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.sonar.api.config.Configuration; +import org.sonar.api.config.Encryption; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.api.config.Settings; + +import static java.util.Collections.unmodifiableMap; +import static java.util.Objects.requireNonNull; + +/** + * In-memory map-based implementation of {@link Settings}. It must be used + * <b>only for unit tests</b>. This is not the implementation + * deployed at runtime, so non-test code must never cast + * {@link Settings} to {@link MapSettings}. + * + * @since 6.1 + */ +public class MapSettings extends Settings { + + private final Map<String, String> props = new HashMap<>(); + private final ConfigurationBridge configurationBridge; + + public MapSettings() { + this(new PropertyDefinitions()); + } + + public MapSettings(PropertyDefinitions definitions) { + super(definitions, new Encryption(null)); + configurationBridge = new ConfigurationBridge(this); + } + + @Override + protected Optional<String> get(String key) { + return Optional.ofNullable(props.get(key)); + } + + @Override + protected void set(String key, String value) { + props.put( + requireNonNull(key, "key can't be null"), + requireNonNull(value, "value can't be null").trim()); + } + + @Override + protected void remove(String key) { + props.remove(key); + } + + @Override + public Map<String, String> getProperties() { + return unmodifiableMap(props); + } + + /** + * Delete all properties + */ + public MapSettings clear() { + props.clear(); + return this; + } + + @Override + public MapSettings setProperty(String key, String value) { + return (MapSettings) super.setProperty(key, value); + } + + @Override + public MapSettings setProperty(String key, Integer value) { + return (MapSettings) super.setProperty(key, value); + } + + @Override + public MapSettings setProperty(String key, Boolean value) { + return (MapSettings) super.setProperty(key, value); + } + + @Override + public MapSettings setProperty(String key, Long value) { + return (MapSettings) super.setProperty(key, value); + } + + /** + * @return a {@link Configuration} proxy on top of this existing {@link Settings} implementation. Changes are reflected in the {@link Configuration} object. + * @since 6.5 + */ + public Configuration asConfig() { + return configurationBridge; + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/MultivalueProperty.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/MultivalueProperty.java new file mode 100644 index 00000000000..3d7d9f009c7 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/MultivalueProperty.java @@ -0,0 +1,208 @@ +/* + * 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.impl.config; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVRecord; +import org.apache.commons.lang.ArrayUtils; + +public class MultivalueProperty { + private MultivalueProperty() { + // prevents instantiation + } + + public static String[] parseAsCsv(String key, String value) { + return parseAsCsv(key, value, Function.identity()); + } + + public static String[] parseAsCsv(String key, String value, Function<String, String> valueProcessor) { + String cleanValue = MultivalueProperty.trimFieldsAndRemoveEmptyFields(value); + List<String> result = new ArrayList<>(); + try (CSVParser csvParser = CSVFormat.RFC4180 + .withHeader((String) null) + .withIgnoreEmptyLines() + .withIgnoreSurroundingSpaces() + .parse(new StringReader(cleanValue))) { + List<CSVRecord> records = csvParser.getRecords(); + if (records.isEmpty()) { + return ArrayUtils.EMPTY_STRING_ARRAY; + } + processRecords(result, records, valueProcessor); + return result.toArray(new String[result.size()]); + } catch (IOException e) { + throw new IllegalStateException("Property: '" + key + "' doesn't contain a valid CSV value: '" + value + "'", e); + } + } + + /** + * In most cases we expect a single record. <br>Having multiple records means the input value was splitted over multiple lines (this is common in Maven). + * For example: + * <pre> + * <sonar.exclusions> + * src/foo, + * src/bar, + * src/biz + * <sonar.exclusions> + * </pre> + * In this case records will be merged to form a single list of items. Last item of a record is appended to first item of next record. + * <p> + * This is a very curious case, but we try to preserve line break in the middle of an item: + * <pre> + * <sonar.exclusions> + * a + * b, + * c + * <sonar.exclusions> + * </pre> + * will produce ['a\nb', 'c'] + */ + private static void processRecords(List<String> result, List<CSVRecord> records, Function<String, String> valueProcessor) { + for (CSVRecord csvRecord : records) { + Iterator<String> it = csvRecord.iterator(); + if (!result.isEmpty()) { + String next = it.next(); + if (!next.isEmpty()) { + int lastItemIdx = result.size() - 1; + String previous = result.get(lastItemIdx); + if (previous.isEmpty()) { + result.set(lastItemIdx, valueProcessor.apply(next)); + } else { + result.set(lastItemIdx, valueProcessor.apply(previous + "\n" + next)); + } + } + } + it.forEachRemaining(s -> { + String apply = valueProcessor.apply(s); + result.add(apply); + }); + } + } + + /** + * Removes the empty fields from the value of a multi-value property from empty fields, including trimming each field. + * <p> + * Quotes can be used to prevent an empty field to be removed (as it is used to preserve empty spaces). + * <ul> + * <li>{@code "" => ""}</li> + * <li>{@code " " => ""}</li> + * <li>{@code "," => ""}</li> + * <li>{@code ",," => ""}</li> + * <li>{@code ",,," => ""}</li> + * <li>{@code ",a" => "a"}</li> + * <li>{@code "a," => "a"}</li> + * <li>{@code ",a," => "a"}</li> + * <li>{@code "a,,b" => "a,b"}</li> + * <li>{@code "a, ,b" => "a,b"}</li> + * <li>{@code "a,\"\",b" => "a,b"}</li> + * <li>{@code "\"a\",\"b\"" => "\"a\",\"b\""}</li> + * <li>{@code "\" a \",\"b \"" => "\" a \",\"b \""}</li> + * <li>{@code "\"a\",\"\",\"b\"" => "\"a\",\"\",\"b\""}</li> + * <li>{@code "\"a\",\" \",\"b\"" => "\"a\",\" \",\"b\""}</li> + * <li>{@code "\" a,,b,c \",\"d \"" => "\" a,,b,c \",\"d \""}</li> + * <li>{@code "a,\" \",b" => "ab"]}</li> + * </ul> + */ + static String trimFieldsAndRemoveEmptyFields(String str) { + char[] chars = str.toCharArray(); + char[] res = new char[chars.length]; + /* + * set when reading the first non trimmable char after a separator char (or the beginning of the string) + * unset when reading a separator + */ + boolean inField = false; + boolean inQuotes = false; + int i = 0; + int resI = 0; + for (; i < chars.length; i++) { + boolean isSeparator = chars[i] == ','; + if (!inQuotes && isSeparator) { + // exiting field (may already be unset) + inField = false; + if (resI > 0) { + resI = retroTrim(res, resI); + } + } else { + boolean isTrimmed = !inQuotes && istrimmable(chars[i]); + if (isTrimmed && !inField) { + // we haven't meet any non trimmable char since the last separator yet + continue; + } + + boolean isEscape = isEscapeChar(chars[i]); + if (isEscape) { + inQuotes = !inQuotes; + } + + // add separator as we already had one field + if (!inField && resI > 0) { + res[resI] = ','; + resI++; + } + + // register in field (may already be set) + inField = true; + // copy current char + res[resI] = chars[i]; + resI++; + } + } + // inQuotes can only be true at this point if quotes are unbalanced + if (!inQuotes) { + // trim end of str + resI = retroTrim(res, resI); + } + return new String(res, 0, resI); + } + + private static boolean isEscapeChar(char aChar) { + return aChar == '"'; + } + + private static boolean istrimmable(char aChar) { + return aChar <= ' '; + } + + /** + * Reads from index {@code resI} to the beginning into {@code res} looking up the location of the trimmable char with + * the lowest index before encountering a non-trimmable char. + * <p> + * This basically trims {@code res} from any trimmable char at its end. + * + * @return index of next location to put new char in res + */ + private static int retroTrim(char[] res, int resI) { + int i = resI; + while (i >= 1) { + if (!istrimmable(res[i - 1])) { + return i; + } + i--; + } + return i; + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/package-info.java new file mode 100644 index 00000000000..bcccfddddd4 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/package-info.java @@ -0,0 +1,23 @@ +/* + * 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.api.impl.config; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/MetadataLoader.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/MetadataLoader.java new file mode 100644 index 00000000000..575fc472592 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/MetadataLoader.java @@ -0,0 +1,78 @@ +/* + * 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.impl.context; + +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Scanner; +import org.sonar.api.SonarEdition; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.Version; + +import static org.apache.commons.lang.StringUtils.trimToEmpty; + +/** + * For internal use + * + * @since 7.8 + */ +public class MetadataLoader { + + private static final String VERSION_FILE_PATH = "/sonar-api-version.txt"; + private static final String EDITION_FILE_PATH = "/sonar-edition.txt"; + + private MetadataLoader() { + // only static methods + } + + public static Version loadVersion(System2 system) { + URL url = system.getResource(VERSION_FILE_PATH); + + try (Scanner scanner = new Scanner(url.openStream(), StandardCharsets.UTF_8.name())) { + String versionInFile = scanner.nextLine(); + return Version.parse(versionInFile); + } catch (IOException e) { + throw new IllegalStateException("Can not load " + VERSION_FILE_PATH + " from classpath ", e); + } + } + + public static SonarEdition loadEdition(System2 system) { + URL url = system.getResource(EDITION_FILE_PATH); + if (url == null) { + return SonarEdition.COMMUNITY; + } + try (Scanner scanner = new Scanner(url.openStream(), StandardCharsets.UTF_8.name())) { + String editionInFile = scanner.nextLine(); + return parseEdition(editionInFile); + } catch (IOException e) { + throw new IllegalStateException("Can not load " + EDITION_FILE_PATH + " from classpath", e); + } + } + + static SonarEdition parseEdition(String edition) { + String str = trimToEmpty(edition.toUpperCase()); + try { + return SonarEdition.valueOf(str); + } catch (IllegalArgumentException e) { + throw new IllegalStateException(String.format("Invalid edition found in '%s': '%s'", EDITION_FILE_PATH, str)); + } + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/PluginContextImpl.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/PluginContextImpl.java new file mode 100644 index 00000000000..211e5cedbc2 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/PluginContextImpl.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.impl.context; + +import org.sonar.api.Plugin; +import org.sonar.api.SonarRuntime; +import org.sonar.api.config.Configuration; +import org.sonar.api.impl.config.MapSettings; + +/** + * Implementation of {@link Plugin.Context} that plugins could use in their unit tests. + * + * Example: + * + * <pre> + * import org.sonar.api.internal.SonarRuntimeImpl; + * import org.sonar.api.config.internal.MapSettings; + * + * ... + * + * SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.create(7, 1), SonarQubeSide.SCANNER); + * MapSettings settings = new MapSettings().setProperty("foo", "bar"); + * Plugin.Context context = new PluginContextImpl.Builder() + * .setSonarRuntime(runtime) + * .setBootConfiguration(settings.asConfig()); + * .build(); + * </pre> + * + * @since 7.1 + */ +public class PluginContextImpl extends Plugin.Context { + + private final Configuration bootConfiguration; + + private PluginContextImpl(Builder builder) { + super(builder.sonarRuntime); + this.bootConfiguration = builder.bootConfiguration != null ? builder.bootConfiguration : new MapSettings().asConfig(); + } + + @Override + public Configuration getBootConfiguration() { + return bootConfiguration; + } + + public static class Builder { + private SonarRuntime sonarRuntime; + private Configuration bootConfiguration; + + /** + * Required. + * @see SonarRuntimeImpl + * @return this + */ + public Builder setSonarRuntime(SonarRuntime r) { + this.sonarRuntime = r; + return this; + } + + /** + * If not set, then an empty configuration is used. + * @return this + */ + public Builder setBootConfiguration(Configuration c) { + this.bootConfiguration = c; + return this; + } + + public Plugin.Context build() { + return new PluginContextImpl(this); + } + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/SonarRuntimeImpl.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/SonarRuntimeImpl.java new file mode 100644 index 00000000000..4e4074efdbb --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/SonarRuntimeImpl.java @@ -0,0 +1,94 @@ +/* + * 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.impl.context; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.SonarEdition; +import org.sonar.api.SonarProduct; +import org.sonar.api.SonarQubeSide; +import org.sonar.api.SonarRuntime; +import org.sonar.api.utils.Version; + +import static java.util.Objects.requireNonNull; +import static org.sonar.api.utils.Preconditions.checkArgument; + +/** + * @since 6.0 + */ +@Immutable +public class SonarRuntimeImpl implements SonarRuntime { + + private final Version version; + private final SonarProduct product; + private final SonarQubeSide sonarQubeSide; + private final SonarEdition edition; + + private SonarRuntimeImpl(Version version, SonarProduct product, @Nullable SonarQubeSide sonarQubeSide, @Nullable SonarEdition edition) { + this.edition = edition; + requireNonNull(product); + checkArgument((product == SonarProduct.SONARQUBE) == (sonarQubeSide != null), "sonarQubeSide should be provided only for SonarQube product"); + checkArgument((product == SonarProduct.SONARQUBE) == (edition != null), "edition should be provided only for SonarQube product"); + this.version = requireNonNull(version); + this.product = product; + this.sonarQubeSide = sonarQubeSide; + } + + @Override + public Version getApiVersion() { + return version; + } + + @Override + public SonarProduct getProduct() { + return product; + } + + @Override + public SonarQubeSide getSonarQubeSide() { + if (sonarQubeSide == null) { + throw new UnsupportedOperationException("Can only be called in SonarQube"); + } + return sonarQubeSide; + } + + @Override + public SonarEdition getEdition() { + if (sonarQubeSide == null) { + throw new UnsupportedOperationException("Can only be called in SonarQube"); + } + return edition; + } + + /** + * Create an instance for SonarQube runtime environment. + */ + public static SonarRuntime forSonarQube(Version version, SonarQubeSide side, SonarEdition edition) { + return new SonarRuntimeImpl(version, SonarProduct.SONARQUBE, side, edition); + } + + /** + * Create an instance for SonarLint runtime environment. + */ + public static SonarRuntime forSonarLint(Version version) { + return new SonarRuntimeImpl(version, SonarProduct.SONARLINT, null, null); + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/package-info.java new file mode 100644 index 00000000000..632005ca779 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/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.api.impl.context; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/AbstractProjectOrModule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/AbstractProjectOrModule.java new file mode 100644 index 00000000000..2834b345a93 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultFileSystem.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultFileSystem.java new file mode 100644 index 00000000000..db015d9277d --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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.impl.fs.predicates.DefaultFilePredicates; +import org.sonar.api.impl.fs.predicates.FileExtensionPredicate; +import org.sonar.api.impl.fs.predicates.OptimizedFilePredicateAdapter; +import org.sonar.api.scan.filesystem.PathResolver; +import org.sonar.api.utils.PathUtils; + +/** + * @since 4.2 + */ +public class DefaultFileSystem implements FileSystem { + + private final Cache cache; + private final FilePredicates predicates; + private final Path baseDir; + private Path workDir; + private Charset encoding; + + /** + * Only for testing + */ + public DefaultFileSystem(Path baseDir) { + this(baseDir, new MapCache(), new DefaultFilePredicates(baseDir)); + } + + /** + * Only for testing + */ + public DefaultFileSystem(File baseDir) { + this(baseDir.toPath(), new MapCache(), new DefaultFilePredicates(baseDir.toPath())); + } + + protected DefaultFileSystem(Path baseDir, Cache cache, FilePredicates filePredicates) { + this.baseDir = baseDir; + this.cache = cache; + this.predicates = filePredicates; + } + + 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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultIndexedFile.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultIndexedFile.java new file mode 100644 index 00000000000..5dae95717c4 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultIndexedFile.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.api.impl.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 java.util.concurrent.atomic.AtomicInteger; +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 static AtomicInteger intGenerator = new AtomicInteger(0); + + 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, intGenerator.getAndIncrement(), + 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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputComponent.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputComponent.java new file mode 100644 index 00000000000..14acba4e1fe --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputDir.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputDir.java new file mode 100644 index 00000000000..53f1800c720 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputFile.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputFile.java new file mode 100644 index 00000000000..61561b0541a --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputFile.java @@ -0,0 +1,439 @@ +/* + * 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.impl.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 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 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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputModule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputModule.java new file mode 100644 index 00000000000..476a719da81 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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.impl.config.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputProject.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputProject.java new file mode 100644 index 00000000000..0ed150065ed --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultTextPointer.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultTextPointer.java new file mode 100644 index 00000000000..541ee958bba --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultTextRange.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultTextRange.java new file mode 100644 index 00000000000..0bf0e5151ab --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/FileMetadata.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/FileMetadata.java new file mode 100644 index 00000000000..8078db10b67 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/FileMetadata.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.api.impl.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.impl.fs.charhandler.CharHandler; +import org.sonar.api.impl.fs.charhandler.FileHashComputer; +import org.sonar.api.impl.fs.charhandler.LineCounter; +import org.sonar.api.impl.fs.charhandler.LineHashComputer; +import org.sonar.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/Metadata.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/Metadata.java new file mode 100644 index 00000000000..e17704135ce --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/Metadata.java @@ -0,0 +1,71 @@ +/* + * 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.impl.fs; + +import java.util.Arrays; + +import javax.annotation.concurrent.Immutable; + +@Immutable +public class Metadata { + private final int lines; + private final int nonBlankLines; + private final String hash; + private final int[] originalLineStartOffsets; + private final int[] originalLineEndOffsets; + private final int lastValidOffset; + + public Metadata(int lines, int nonBlankLines, String hash, int[] originalLineStartOffsets, int[] originalLineEndOffsets, int lastValidOffset) { + this.lines = lines; + this.nonBlankLines = nonBlankLines; + this.hash = hash; + this.originalLineStartOffsets = Arrays.copyOf(originalLineStartOffsets, originalLineStartOffsets.length); + this.originalLineEndOffsets = Arrays.copyOf(originalLineEndOffsets, originalLineEndOffsets.length); + this.lastValidOffset = lastValidOffset; + } + + public int lines() { + return lines; + } + + public int nonBlankLines() { + return nonBlankLines; + } + + public String hash() { + return hash; + } + + public int[] originalLineStartOffsets() { + return originalLineStartOffsets; + } + + public int[] originalLineEndOffsets() { + return originalLineEndOffsets; + } + + public int lastValidOffset() { + return lastValidOffset; + } + + public boolean isEmpty() { + return lastValidOffset == 0; + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/PathPattern.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/PathPattern.java new file mode 100644 index 00000000000..30a813d5a60 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/SensorStrategy.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/SensorStrategy.java new file mode 100644 index 00000000000..d956d061f91 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/TestInputFileBuilder.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/TestInputFileBuilder.java new file mode 100644 index 00000000000..e731e7ff7ab --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/TestInputFileBuilder.java @@ -0,0 +1,278 @@ +/* + * 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.impl.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.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/CharHandler.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/CharHandler.java new file mode 100644 index 00000000000..06218a6a9f2 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/FileHashComputer.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/FileHashComputer.java new file mode 100644 index 00000000000..eaa5672eef6 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/IntArrayList.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/IntArrayList.java new file mode 100644 index 00000000000..b298b0524c5 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineCounter.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineCounter.java new file mode 100644 index 00000000000..ba5093b29e8 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineHashComputer.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineHashComputer.java new file mode 100644 index 00000000000..8384258161c --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineOffsetCounter.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineOffsetCounter.java new file mode 100644 index 00000000000..1b0ad31fed1 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/package-info.java new file mode 100644 index 00000000000..9c9bce61dbc --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.fs.charhandler; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/package-info.java new file mode 100644 index 00000000000..10a797893e5 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.fs; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AbsolutePathPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AbsolutePathPredicate.java new file mode 100644 index 00000000000..c83cc6463a1 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AbstractFilePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AbstractFilePredicate.java new file mode 100644 index 00000000000..1a964b49b6b --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AndPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AndPredicate.java new file mode 100644 index 00000000000..d2ea1f35f59 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/DefaultFilePredicates.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/DefaultFilePredicates.java new file mode 100644 index 00000000000..fe005c608e7 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FalsePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FalsePredicate.java new file mode 100644 index 00000000000..d6c1d78a696 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FileExtensionPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FileExtensionPredicate.java new file mode 100644 index 00000000000..7775c567832 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FilenamePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FilenamePredicate.java new file mode 100644 index 00000000000..94fac58852f --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/LanguagePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/LanguagePredicate.java new file mode 100644 index 00000000000..e553625789a --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/NotPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/NotPredicate.java new file mode 100644 index 00000000000..4841c558826 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/NotPredicate.java @@ -0,0 +1,48 @@ +/* + * 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.impl.fs.predicates; + +import java.util.Arrays; +import java.util.List; +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.InputFile; + +/** + * @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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OperatorPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OperatorPredicate.java new file mode 100644 index 00000000000..bf8e566e144 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OptimizedFilePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OptimizedFilePredicate.java new file mode 100644 index 00000000000..aaba4c890d6 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OptimizedFilePredicateAdapter.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OptimizedFilePredicateAdapter.java new file mode 100644 index 00000000000..5de4fee72b5 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OrPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OrPredicate.java new file mode 100644 index 00000000000..3f93903c4da --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/PathPatternPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/PathPatternPredicate.java new file mode 100644 index 00000000000..1e276751134 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.fs.predicates; + +import java.nio.file.Paths; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/RelativePathPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/RelativePathPredicate.java new file mode 100644 index 00000000000..a1cb9f42090 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/StatusPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/StatusPredicate.java new file mode 100644 index 00000000000..bcac4e04c49 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/StatusPredicate.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.api.impl.fs.predicates; + +import javax.annotation.Nullable; +import org.sonar.api.batch.fs.InputFile; + +/** + * @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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/TruePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/TruePredicate.java new file mode 100644 index 00000000000..04d56ee387e --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/TypePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/TypePredicate.java new file mode 100644 index 00000000000..f9d57058352 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/URIPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/URIPredicate.java new file mode 100644 index 00000000000..60fea977b5d --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/package-info.java new file mode 100644 index 00000000000..aa33a1e9028 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/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.api.impl.fs.predicates; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/AbstractDefaultIssue.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/AbstractDefaultIssue.java new file mode 100644 index 00000000000..8705bf2fda0 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.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.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.impl.fs.DefaultInputDir; +import org.sonar.api.impl.fs.DefaultInputModule; +import org.sonar.api.impl.fs.DefaultInputProject; +import org.sonar.api.impl.sensor.DefaultStorable; +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-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultIssue.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultIssue.java new file mode 100644 index 00000000000..aebe8f90a61 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.issue; + +import javax.annotation.Nullable; +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.impl.fs.DefaultInputProject; +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-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultIssueLocation.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultIssueLocation.java new file mode 100644 index 00000000000..6b2329c81c2 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.api.impl.issue; + +import javax.annotation.Nullable; +import org.sonar.api.batch.fs.InputComponent; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.batch.sensor.issue.IssueLocation; +import org.sonar.api.batch.sensor.issue.NewIssueLocation; +import org.sonar.api.impl.fs.DefaultInputFile; + +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-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultNoSonarFilter.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultNoSonarFilter.java new file mode 100644 index 00000000000..3bef1e9e5ef --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultNoSonarFilter.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.api.impl.issue; + +import java.util.Set; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.issue.NoSonarFilter; +import org.sonar.api.impl.fs.DefaultInputFile; + +public class DefaultNoSonarFilter extends NoSonarFilter { + public NoSonarFilter noSonarInFile(InputFile inputFile, Set<Integer> noSonarLines) { + ((DefaultInputFile) inputFile).noSonarAt(noSonarLines); + return this; + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/package-info.java new file mode 100644 index 00000000000..8d4146ce55a --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/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.api.impl.issue; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/ActiveRulesBuilder.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/ActiveRulesBuilder.java new file mode 100644 index 00000000000..318a4738291 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/ActiveRulesBuilder.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.api.impl.rule; + +import java.util.LinkedHashMap; +import java.util.Map; +import org.sonar.api.batch.rule.ActiveRules; +import org.sonar.api.batch.rule.NewActiveRule; +import org.sonar.api.rule.RuleKey; + +/** + * Builds instances of {@link org.sonar.api.batch.rule.ActiveRules}. + * <b>For unit testing and internal use only</b>. + * + * @since 4.2 + */ +public class ActiveRulesBuilder { + + private final Map<RuleKey, NewActiveRule> map = new LinkedHashMap<>(); + + public ActiveRulesBuilder addRule(NewActiveRule newActiveRule) { + if (map.containsKey(newActiveRule.ruleKey())) { + throw new IllegalStateException(String.format("Rule '%s' is already activated", newActiveRule.ruleKey())); + } + map.put(newActiveRule.ruleKey(), newActiveRule); + return this; + } + + public ActiveRules build() { + return new DefaultActiveRules(map.values()); + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/DefaultActiveRules.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/DefaultActiveRules.java new file mode 100644 index 00000000000..9add14cc484 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/DefaultActiveRules.java @@ -0,0 +1,85 @@ +/* + * 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.impl.rule; + +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.ActiveRule; +import org.sonar.api.batch.rule.ActiveRules; +import org.sonar.api.batch.rule.DefaultActiveRule; +import org.sonar.api.batch.rule.NewActiveRule; +import org.sonar.api.rule.RuleKey; + +@Immutable +public class DefaultActiveRules implements ActiveRules { + private final Map<String, List<ActiveRule>> activeRulesByRepository = new HashMap<>(); + private final Map<String, Map<String, ActiveRule>> activeRulesByRepositoryAndKey = new HashMap<>(); + private final Map<String, Map<String, ActiveRule>> activeRulesByRepositoryAndInternalKey = new HashMap<>(); + private final Map<String, List<ActiveRule>> activeRulesByLanguage = new HashMap<>(); + + public DefaultActiveRules(Collection<NewActiveRule> newActiveRules) { + for (NewActiveRule newAR : newActiveRules) { + DefaultActiveRule ar = new DefaultActiveRule(newAR); + String repo = ar.ruleKey().repository(); + activeRulesByRepository.computeIfAbsent(repo, x -> new ArrayList<>()).add(ar); + if (ar.language() != null) { + activeRulesByLanguage.computeIfAbsent(ar.language(), x -> new ArrayList<>()).add(ar); + } + + activeRulesByRepositoryAndKey.computeIfAbsent(repo, r -> new HashMap<>()).put(ar.ruleKey().rule(), ar); + String internalKey = ar.internalKey(); + if (internalKey != null) { + activeRulesByRepositoryAndInternalKey.computeIfAbsent(repo, r -> new HashMap<>()).put(internalKey, ar); + } + } + } + + @Override + public ActiveRule find(RuleKey ruleKey) { + return activeRulesByRepositoryAndKey.getOrDefault(ruleKey.repository(), Collections.emptyMap()) + .get(ruleKey.rule()); + } + + @Override + public Collection<ActiveRule> findAll() { + return activeRulesByRepository.entrySet().stream().flatMap(x -> x.getValue().stream()).collect(Collectors.toList()); + } + + @Override + public Collection<ActiveRule> findByRepository(String repository) { + return activeRulesByRepository.getOrDefault(repository, Collections.emptyList()); + } + + @Override + public Collection<ActiveRule> findByLanguage(String language) { + return activeRulesByLanguage.getOrDefault(language, Collections.emptyList()); + } + + @Override + public ActiveRule findByInternalKey(String repository, String internalKey) { + return activeRulesByRepositoryAndInternalKey.containsKey(repository) ? activeRulesByRepositoryAndInternalKey.get(repository).get(internalKey) : null; + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/DefaultRules.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/DefaultRules.java new file mode 100644 index 00000000000..c7c7b5ead44 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/DefaultRules.java @@ -0,0 +1,91 @@ +/* + * 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.impl.rule; + +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.DefaultRule; +import org.sonar.api.batch.rule.NewRule; +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-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/RulesBuilder.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/RulesBuilder.java new file mode 100644 index 00000000000..36c84bef34a --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/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.impl.rule; + +import java.util.HashMap; +import java.util.Map; +import org.sonar.api.batch.rule.NewRule; +import org.sonar.api.batch.rule.Rules; +import org.sonar.api.rule.RuleKey; + +/** + * 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-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultAdHocRule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultAdHocRule.java new file mode 100644 index 00000000000..1513fa2f695 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultAdHocRule.java @@ -0,0 +1,126 @@ +/* + * 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.impl.sensor; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.batch.rule.Severity; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.batch.sensor.rule.AdHocRule; +import org.sonar.api.batch.sensor.rule.NewAdHocRule; +import org.sonar.api.rules.RuleType; + +import static org.apache.commons.lang.StringUtils.isNotBlank; +import static org.sonar.api.utils.Preconditions.checkState; + +public class DefaultAdHocRule extends DefaultStorable implements AdHocRule, NewAdHocRule { + private Severity severity; + private RuleType type; + private String name; + private String description; + private String engineId; + private String ruleId; + + public DefaultAdHocRule() { + super(null); + } + + public DefaultAdHocRule(@Nullable SensorStorage storage) { + super(storage); + } + + @Override + public DefaultAdHocRule severity(Severity severity) { + this.severity = severity; + return this; + } + + @Override + public String engineId() { + return engineId; + } + + @Override + public String ruleId() { + return ruleId; + } + + @Override + public String name() { + return name; + } + + @CheckForNull + @Override + public String description() { + return description; + } + + @Override + public Severity severity() { + return this.severity; + } + + @Override + public void doSave() { + checkState(isNotBlank(engineId), "Engine id is mandatory on ad hoc rule"); + checkState(isNotBlank(ruleId), "Rule id is mandatory on ad hoc rule"); + checkState(isNotBlank(name), "Name is mandatory on every ad hoc rule"); + checkState(severity != null, "Severity is mandatory on every ad hoc rule"); + checkState(type != null, "Type is mandatory on every ad hoc rule"); + storage.store(this); + } + + @Override + public RuleType type() { + return type; + } + + @Override + public DefaultAdHocRule engineId(String engineId) { + this.engineId = engineId; + return this; + } + + @Override + public DefaultAdHocRule ruleId(String ruleId) { + this.ruleId = ruleId; + return this; + } + + @Override + public DefaultAdHocRule name(String name) { + this.name = name; + return this; + } + + @Override + public DefaultAdHocRule description(@Nullable String description) { + this.description = description; + return this; + } + + @Override + public DefaultAdHocRule type(RuleType type) { + this.type = type; + return this; + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultAnalysisError.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultAnalysisError.java new file mode 100644 index 00000000000..5508461ce68 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultAnalysisError.java @@ -0,0 +1,87 @@ +/* + * 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.impl.sensor; + +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.TextPointer; +import org.sonar.api.batch.sensor.error.AnalysisError; +import org.sonar.api.batch.sensor.error.NewAnalysisError; +import org.sonar.api.batch.sensor.internal.SensorStorage; + +import static java.util.Objects.requireNonNull; +import static org.sonar.api.utils.Preconditions.checkArgument; +import static org.sonar.api.utils.Preconditions.checkState; + +public class DefaultAnalysisError extends DefaultStorable implements NewAnalysisError, AnalysisError { + private InputFile inputFile; + private String message; + private TextPointer location; + + public DefaultAnalysisError() { + super(null); + } + + public DefaultAnalysisError(SensorStorage storage) { + super(storage); + } + + @Override + public InputFile inputFile() { + return inputFile; + } + + @Override + public String message() { + return message; + } + + @Override + public TextPointer location() { + return location; + } + + @Override + public NewAnalysisError onFile(InputFile inputFile) { + checkArgument(inputFile != null, "Cannot use a inputFile that is null"); + checkState(this.inputFile == null, "onFile() already called"); + this.inputFile = inputFile; + return this; + } + + @Override + public NewAnalysisError message(String message) { + this.message = message; + return this; + } + + @Override + public NewAnalysisError at(TextPointer location) { + checkState(this.location == null, "at() already called"); + this.location = location; + return this; + } + + @Override + protected void doSave() { + requireNonNull(this.inputFile, "inputFile is mandatory on AnalysisError"); + storage.store(this); + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultCoverage.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultCoverage.java new file mode 100644 index 00000000000..c4149ec2f63 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultCoverage.java @@ -0,0 +1,157 @@ +/* + * 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.impl.sensor; + +import java.util.Collections; +import java.util.SortedMap; +import java.util.TreeMap; +import javax.annotation.Nullable; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.coverage.CoverageType; +import org.sonar.api.batch.sensor.coverage.NewCoverage; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.impl.fs.DefaultInputFile; + +import static java.util.Objects.requireNonNull; +import static org.sonar.api.utils.Preconditions.checkState; + +public class DefaultCoverage extends DefaultStorable implements NewCoverage { + + private InputFile inputFile; + private CoverageType type; + private int totalCoveredLines = 0; + private int totalConditions = 0; + private int totalCoveredConditions = 0; + private SortedMap<Integer, Integer> hitsByLine = new TreeMap<>(); + private SortedMap<Integer, Integer> conditionsByLine = new TreeMap<>(); + private SortedMap<Integer, Integer> coveredConditionsByLine = new TreeMap<>(); + + public DefaultCoverage() { + super(); + } + + public DefaultCoverage(@Nullable SensorStorage storage) { + super(storage); + } + + @Override + public DefaultCoverage onFile(InputFile inputFile) { + this.inputFile = inputFile; + return this; + } + + public InputFile inputFile() { + return inputFile; + } + + @Override + public NewCoverage ofType(CoverageType type) { + this.type = requireNonNull(type, "type can't be null"); + return this; + } + + public CoverageType type() { + return type; + } + + @Override + public NewCoverage lineHits(int line, int hits) { + validateFile(); + if (isExcluded()) { + return this; + } + validateLine(line); + + if (!hitsByLine.containsKey(line)) { + hitsByLine.put(line, hits); + if (hits > 0) { + totalCoveredLines += 1; + } + } + return this; + } + + private void validateLine(int line) { + checkState(line <= inputFile.lines(), "Line %s is out of range in the file %s (lines: %s)", line, inputFile, inputFile.lines()); + checkState(line > 0, "Line number must be strictly positive: %s", line); + } + + private void validateFile() { + requireNonNull(inputFile, "Call onFile() first"); + } + + @Override + public NewCoverage conditions(int line, int conditions, int coveredConditions) { + validateFile(); + if (isExcluded()) { + return this; + } + validateLine(line); + + if (conditions > 0 && !conditionsByLine.containsKey(line)) { + totalConditions += conditions; + totalCoveredConditions += coveredConditions; + conditionsByLine.put(line, conditions); + coveredConditionsByLine.put(line, coveredConditions); + } + return this; + } + + public int coveredLines() { + return totalCoveredLines; + } + + public int linesToCover() { + return hitsByLine.size(); + } + + public int conditions() { + return totalConditions; + } + + public int coveredConditions() { + return totalCoveredConditions; + } + + public SortedMap<Integer, Integer> hitsByLine() { + return Collections.unmodifiableSortedMap(hitsByLine); + } + + public SortedMap<Integer, Integer> conditionsByLine() { + return Collections.unmodifiableSortedMap(conditionsByLine); + } + + public SortedMap<Integer, Integer> coveredConditionsByLine() { + return Collections.unmodifiableSortedMap(coveredConditionsByLine); + } + + @Override + public void doSave() { + validateFile(); + if (!isExcluded()) { + storage.store(this); + } + } + + private boolean isExcluded() { + return ((DefaultInputFile) inputFile).isExcludedForCoverage(); + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultCpdTokens.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultCpdTokens.java new file mode 100644 index 00000000000..9b789d22d14 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultCpdTokens.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.api.impl.sensor; + +import java.util.ArrayList; +import java.util.List; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.batch.sensor.cpd.NewCpdTokens; +import org.sonar.api.batch.sensor.cpd.internal.TokensLine; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.impl.fs.DefaultInputFile; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import static java.util.Collections.unmodifiableList; +import static java.util.Objects.requireNonNull; +import static org.sonar.api.utils.Preconditions.checkState; + +public class DefaultCpdTokens extends DefaultStorable implements NewCpdTokens { + private static final Logger LOG = Loggers.get(DefaultCpdTokens.class); + private final List<TokensLine> result = new ArrayList<>(); + private DefaultInputFile inputFile; + private int startLine = Integer.MIN_VALUE; + private int startIndex = 0; + private int currentIndex = 0; + private StringBuilder sb = new StringBuilder(); + private TextRange lastRange; + private boolean loggedTestCpdWarning = false; + + public DefaultCpdTokens(SensorStorage storage) { + super(storage); + } + + @Override + public DefaultCpdTokens onFile(InputFile inputFile) { + this.inputFile = (DefaultInputFile) requireNonNull(inputFile, "file can't be null"); + return this; + } + + public InputFile inputFile() { + return inputFile; + } + + @Override + public NewCpdTokens addToken(int startLine, int startLineOffset, int endLine, int endLineOffset, String image) { + checkInputFileNotNull(); + TextRange newRange; + try { + newRange = inputFile.newRange(startLine, startLineOffset, endLine, endLineOffset); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to register token in file " + inputFile, e); + } + return addToken(newRange, image); + } + + @Override + public DefaultCpdTokens addToken(TextRange range, String image) { + requireNonNull(range, "Range should not be null"); + requireNonNull(image, "Image should not be null"); + checkInputFileNotNull(); + if (isExcludedForDuplication()) { + return this; + } + checkState(lastRange == null || lastRange.end().compareTo(range.start()) <= 0, + "Tokens of file %s should be provided in order.\nPrevious token: %s\nLast token: %s", inputFile, lastRange, range); + + int line = range.start().line(); + if (line != startLine) { + addNewTokensLine(result, startIndex, currentIndex, startLine, sb); + startIndex = currentIndex + 1; + startLine = line; + } + currentIndex++; + sb.append(image); + lastRange = range; + + return this; + } + + private boolean isExcludedForDuplication() { + if (inputFile.isExcludedForDuplication()) { + return true; + } + if (inputFile.type() == InputFile.Type.TEST) { + if (!loggedTestCpdWarning) { + LOG.warn("Duplication reported for '{}' will be ignored because it's a test file.", inputFile); + loggedTestCpdWarning = true; + } + return true; + } + return false; + } + + public List<TokensLine> getTokenLines() { + return unmodifiableList(new ArrayList<>(result)); + } + + private static void addNewTokensLine(List<TokensLine> result, int startUnit, int endUnit, int startLine, StringBuilder sb) { + if (sb.length() != 0) { + result.add(new TokensLine(startUnit, endUnit, startLine, sb.toString())); + sb.setLength(0); + } + } + + @Override + protected void doSave() { + checkState(inputFile != null, "Call onFile() first"); + if (isExcludedForDuplication()) { + return; + } + addNewTokensLine(result, startIndex, currentIndex, startLine, sb); + storage.store(this); + } + + private void checkInputFileNotNull() { + checkState(inputFile != null, "Call onFile() first"); + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultExternalIssue.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultExternalIssue.java new file mode 100644 index 00000000000..ae17adbaca6 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultExternalIssue.java @@ -0,0 +1,135 @@ +/* + * 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.impl.sensor; + +import javax.annotation.Nullable; +import org.sonar.api.batch.rule.Severity; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.batch.sensor.issue.ExternalIssue; +import org.sonar.api.batch.sensor.issue.NewExternalIssue; +import org.sonar.api.impl.fs.DefaultInputProject; +import org.sonar.api.impl.issue.AbstractDefaultIssue; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.RuleType; + +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 DefaultExternalIssue extends AbstractDefaultIssue<DefaultExternalIssue> implements ExternalIssue, NewExternalIssue { + private Long effort; + private Severity severity; + private RuleType type; + private String engineId; + private String ruleId; + + public DefaultExternalIssue(DefaultInputProject project) { + this(project, null); + } + + public DefaultExternalIssue(DefaultInputProject project, @Nullable SensorStorage storage) { + super(project, storage); + } + + @Override + public DefaultExternalIssue remediationEffortMinutes(@Nullable Long effort) { + checkArgument(effort == null || effort >= 0, format("effort must be greater than or equal 0 (got %s)", effort)); + this.effort = effort; + return this; + } + + @Override + public DefaultExternalIssue severity(Severity severity) { + this.severity = severity; + return this; + } + + @Override + public String engineId() { + return engineId; + } + + @Override + public String ruleId() { + return ruleId; + } + + @Override + public Severity severity() { + return this.severity; + } + + @Override + public Long remediationEffort() { + return this.effort; + } + + @Override + public void doSave() { + requireNonNull(this.engineId, "Engine id is mandatory on external issue"); + requireNonNull(this.ruleId, "Rule id is mandatory on external issue"); + checkState(primaryLocation != null, "Primary location is mandatory on every external issue"); + checkState(primaryLocation.inputComponent().isFile(), "External issues must be located in files"); + checkState(primaryLocation.message() != null, "External issues must have a message"); + checkState(severity != null, "Severity is mandatory on every external issue"); + checkState(type != null, "Type is mandatory on every external issue"); + storage.store(this); + } + + @Override + public RuleType type() { + return type; + } + + @Override + public NewExternalIssue engineId(String engineId) { + this.engineId = engineId; + return this; + } + + @Override + public NewExternalIssue ruleId(String ruleId) { + this.ruleId = ruleId; + return this; + } + + @Override + public DefaultExternalIssue forRule(RuleKey ruleKey) { + this.engineId = ruleKey.repository(); + this.ruleId = ruleKey.rule(); + return this; + } + + @Override + public RuleKey ruleKey() { + if (engineId != null && ruleId != null) { + return RuleKey.of(RuleKey.EXTERNAL_RULE_REPO_PREFIX + engineId, ruleId); + } + return null; + } + + @Override + public DefaultExternalIssue type(RuleType type) { + this.type = type; + return this; + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultHighlighting.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultHighlighting.java new file mode 100644 index 00000000000..c7e133dfea7 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultHighlighting.java @@ -0,0 +1,126 @@ +/* + * 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.impl.sensor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.batch.sensor.highlighting.NewHighlighting; +import org.sonar.api.batch.sensor.highlighting.TypeOfText; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.impl.fs.DefaultInputFile; + +import static java.util.Objects.requireNonNull; +import static org.sonar.api.utils.Preconditions.checkState; + +public class DefaultHighlighting extends DefaultStorable implements NewHighlighting { + + private final List<SyntaxHighlightingRule> syntaxHighlightingRules; + private DefaultInputFile inputFile; + + public DefaultHighlighting(SensorStorage storage) { + super(storage); + syntaxHighlightingRules = new ArrayList<>(); + } + + public List<SyntaxHighlightingRule> getSyntaxHighlightingRuleSet() { + return syntaxHighlightingRules; + } + + private void checkOverlappingBoundaries() { + if (syntaxHighlightingRules.size() > 1) { + Iterator<SyntaxHighlightingRule> it = syntaxHighlightingRules.iterator(); + SyntaxHighlightingRule previous = it.next(); + while (it.hasNext()) { + SyntaxHighlightingRule current = it.next(); + if (previous.range().end().compareTo(current.range().start()) > 0 && (previous.range().end().compareTo(current.range().end()) < 0)) { + String errorMsg = String.format("Cannot register highlighting rule for characters at %s as it " + + "overlaps at least one existing rule", current.range()); + throw new IllegalStateException(errorMsg); + } + previous = current; + } + } + } + + @Override + public DefaultHighlighting onFile(InputFile inputFile) { + requireNonNull(inputFile, "file can't be null"); + this.inputFile = (DefaultInputFile) inputFile; + return this; + } + + public InputFile inputFile() { + return inputFile; + } + + @Override + public DefaultHighlighting highlight(int startOffset, int endOffset, TypeOfText typeOfText) { + checkInputFileNotNull(); + TextRange newRange; + try { + newRange = inputFile.newRange(startOffset, endOffset); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to highlight file " + inputFile, e); + } + return highlight(newRange, typeOfText); + } + + @Override + public DefaultHighlighting highlight(int startLine, int startLineOffset, int endLine, int endLineOffset, TypeOfText typeOfText) { + checkInputFileNotNull(); + TextRange newRange; + try { + newRange = inputFile.newRange(startLine, startLineOffset, endLine, endLineOffset); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to highlight file " + inputFile, e); + } + return highlight(newRange, typeOfText); + } + + @Override + public DefaultHighlighting highlight(TextRange range, TypeOfText typeOfText) { + SyntaxHighlightingRule syntaxHighlightingRule = SyntaxHighlightingRule.create(range, typeOfText); + this.syntaxHighlightingRules.add(syntaxHighlightingRule); + return this; + } + + @Override + protected void doSave() { + checkInputFileNotNull(); + // Sort rules to avoid variation during consecutive runs + Collections.sort(syntaxHighlightingRules, (left, right) -> { + int result = left.range().start().compareTo(right.range().start()); + if (result == 0) { + result = right.range().end().compareTo(left.range().end()); + } + return result; + }); + checkOverlappingBoundaries(); + storage.store(this); + } + + private void checkInputFileNotNull() { + checkState(inputFile != null, "Call onFile() first"); + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultMeasure.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultMeasure.java new file mode 100644 index 00000000000..d2919aba182 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultMeasure.java @@ -0,0 +1,139 @@ +/* + * 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.impl.sensor; + +import java.io.Serializable; +import javax.annotation.Nullable; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.sonar.api.batch.fs.InputComponent; +import org.sonar.api.batch.measure.Metric; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.batch.sensor.measure.Measure; +import org.sonar.api.batch.sensor.measure.NewMeasure; + +import static java.util.Objects.requireNonNull; +import static org.sonar.api.utils.Preconditions.checkArgument; +import static org.sonar.api.utils.Preconditions.checkState; + +public class DefaultMeasure<G extends Serializable> extends DefaultStorable implements Measure<G>, NewMeasure<G> { + + private InputComponent component; + private Metric<G> metric; + private G value; + private boolean fromCore = false; + + public DefaultMeasure() { + super(); + } + + public DefaultMeasure(@Nullable SensorStorage storage) { + super(storage); + } + + @Override + public DefaultMeasure<G> 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 DefaultMeasure<G> forMetric(Metric<G> metric) { + checkState(this.metric == null, "Metric already defined"); + requireNonNull(metric, "metric should be non null"); + this.metric = metric; + return this; + } + + @Override + public DefaultMeasure<G> withValue(G value) { + checkState(this.value == null, "Measure value already defined"); + requireNonNull(value, "Measure value can't be null"); + this.value = value; + return this; + } + + /** + * For internal use. + */ + public boolean isFromCore() { + return fromCore; + } + + /** + * For internal use. Used by core components to bypass check that prevent a plugin to store core measures. + */ + public DefaultMeasure<G> setFromCore() { + this.fromCore = true; + return this; + } + + @Override + public void doSave() { + requireNonNull(this.value, "Measure value can't be null"); + requireNonNull(this.metric, "Measure metric can't be null"); + checkState(this.metric.valueType().equals(this.value.getClass()), "Measure value should be of type %s", this.metric.valueType()); + storage.store(this); + } + + @Override + public Metric<G> metric() { + return metric; + } + + @Override + public InputComponent inputComponent() { + return component; + } + + @Override + public G value() { + return value; + } + + // For testing purpose + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (obj.getClass() != getClass()) { + return false; + } + DefaultMeasure<?> rhs = (DefaultMeasure<?>) obj; + return new EqualsBuilder() + .append(component, rhs.component) + .append(metric, rhs.metric) + .append(value, rhs.value) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(27, 45).append(component).append(metric).append(value).toHashCode(); + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultPostJobDescriptor.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultPostJobDescriptor.java new file mode 100644 index 00000000000..152c5719e0a --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultPostJobDescriptor.java @@ -0,0 +1,56 @@ +/* + * 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.impl.sensor; + +import java.util.Arrays; +import java.util.Collection; +import org.sonar.api.batch.postjob.PostJobDescriptor; + +public class DefaultPostJobDescriptor implements PostJobDescriptor { + + private String name; + private String[] properties = new String[0]; + + public String name() { + return name; + } + + public Collection<String> properties() { + return Arrays.asList(properties); + } + + @Override + public DefaultPostJobDescriptor name(String name) { + this.name = name; + return this; + } + + @Override + public DefaultPostJobDescriptor requireProperty(String... propertyKey) { + return requireProperties(propertyKey); + } + + @Override + public DefaultPostJobDescriptor requireProperties(String... propertyKeys) { + this.properties = propertyKeys; + return this; + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSensorDescriptor.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSensorDescriptor.java new file mode 100644 index 00000000000..5b60ac287e2 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSensorDescriptor.java @@ -0,0 +1,123 @@ +/* + * 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.impl.sensor; + +import java.util.Arrays; +import java.util.Collection; +import java.util.function.Predicate; +import javax.annotation.Nullable; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.config.Configuration; + +import static java.util.Arrays.asList; + +public class DefaultSensorDescriptor implements SensorDescriptor { + + private String name; + private String[] languages = new String[0]; + private InputFile.Type type = null; + private String[] ruleRepositories = new String[0]; + private boolean global = false; + private Predicate<Configuration> configurationPredicate; + + public String name() { + return name; + } + + public Collection<String> languages() { + return Arrays.asList(languages); + } + + @Nullable + public InputFile.Type type() { + return type; + } + + public Collection<String> ruleRepositories() { + return Arrays.asList(ruleRepositories); + } + + public Predicate<Configuration> configurationPredicate() { + return configurationPredicate; + } + + public boolean isGlobal() { + return global; + } + + @Override + public DefaultSensorDescriptor name(String name) { + this.name = name; + return this; + } + + @Override + public DefaultSensorDescriptor onlyOnLanguage(String languageKey) { + return onlyOnLanguages(languageKey); + } + + @Override + public DefaultSensorDescriptor onlyOnLanguages(String... languageKeys) { + this.languages = languageKeys; + return this; + } + + @Override + public DefaultSensorDescriptor onlyOnFileType(InputFile.Type type) { + this.type = type; + return this; + } + + @Override + public DefaultSensorDescriptor createIssuesForRuleRepository(String... repositoryKey) { + return createIssuesForRuleRepositories(repositoryKey); + } + + @Override + public DefaultSensorDescriptor createIssuesForRuleRepositories(String... repositoryKeys) { + this.ruleRepositories = repositoryKeys; + return this; + } + + @Override + public DefaultSensorDescriptor requireProperty(String... propertyKey) { + return requireProperties(propertyKey); + } + + @Override + public DefaultSensorDescriptor requireProperties(String... propertyKeys) { + this.configurationPredicate = config -> asList(propertyKeys).stream().allMatch(config::hasKey); + return this; + } + + @Override + public SensorDescriptor global() { + this.global = true; + return this; + } + + @Override + public SensorDescriptor onlyWhenConfiguration(Predicate<Configuration> configurationPredicate) { + this.configurationPredicate = configurationPredicate; + return this; + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSignificantCode.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSignificantCode.java new file mode 100644 index 00000000000..6a373ac061c --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSignificantCode.java @@ -0,0 +1,75 @@ +/* + * 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.impl.sensor; + +import java.util.SortedMap; +import java.util.TreeMap; +import javax.annotation.Nullable; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.batch.sensor.code.NewSignificantCode; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.utils.Preconditions; + +public class DefaultSignificantCode extends DefaultStorable implements NewSignificantCode { + private SortedMap<Integer, TextRange> significantCodePerLine = new TreeMap<>(); + private InputFile inputFile; + + public DefaultSignificantCode() { + super(); + } + + public DefaultSignificantCode(@Nullable SensorStorage storage) { + super(storage); + } + + @Override + public DefaultSignificantCode onFile(InputFile inputFile) { + this.inputFile = inputFile; + return this; + } + + @Override + public DefaultSignificantCode addRange(TextRange range) { + Preconditions.checkState(this.inputFile != null, "addRange() should be called after on()"); + + int line = range.start().line(); + + Preconditions.checkArgument(line == range.end().line(), "Ranges of significant code must be located in a single line"); + Preconditions.checkState(!significantCodePerLine.containsKey(line), "Significant code was already reported for line '%s'. Can only report once per line.", line); + + significantCodePerLine.put(line, range); + return this; + } + + @Override + protected void doSave() { + Preconditions.checkState(inputFile != null, "Call onFile() first"); + storage.store(this); + } + + public InputFile inputFile() { + return inputFile; + } + + public SortedMap<Integer, TextRange> significantCodePerLine() { + return significantCodePerLine; + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultStorable.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultStorable.java new file mode 100644 index 00000000000..c4f6ca39f4a --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultStorable.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.api.impl.sensor; + +import javax.annotation.Nullable; +import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang.builder.ToStringStyle; +import org.sonar.api.batch.sensor.internal.SensorStorage; + +import static java.util.Objects.requireNonNull; +import static org.sonar.api.utils.Preconditions.checkState; + +public abstract class DefaultStorable { + + protected final transient SensorStorage storage; + private transient boolean saved = false; + + public DefaultStorable() { + this.storage = null; + } + + public DefaultStorable(@Nullable SensorStorage storage) { + this.storage = storage; + } + + public final void save() { + requireNonNull(this.storage, "No persister on this object"); + checkState(!saved, "This object was already saved"); + doSave(); + this.saved = true; + } + + protected abstract void doSave(); + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSymbolTable.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSymbolTable.java new file mode 100644 index 00000000000..73ec86450a5 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSymbolTable.java @@ -0,0 +1,148 @@ +/* + * 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.impl.sensor; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.batch.sensor.symbol.NewSymbol; +import org.sonar.api.batch.sensor.symbol.NewSymbolTable; +import org.sonar.api.impl.fs.DefaultInputFile; + +import static java.util.Objects.requireNonNull; +import static org.sonar.api.utils.Preconditions.checkArgument; +import static org.sonar.api.utils.Preconditions.checkState; + +public class DefaultSymbolTable extends DefaultStorable implements NewSymbolTable { + + private final Map<TextRange, Set<TextRange>> referencesBySymbol; + private DefaultInputFile inputFile; + + public DefaultSymbolTable(SensorStorage storage) { + super(storage); + referencesBySymbol = new LinkedHashMap<>(); + } + + public Map<TextRange, Set<TextRange>> getReferencesBySymbol() { + return referencesBySymbol; + } + + @Override + public DefaultSymbolTable onFile(InputFile inputFile) { + requireNonNull(inputFile, "file can't be null"); + this.inputFile = (DefaultInputFile) inputFile; + return this; + } + + public InputFile inputFile() { + return inputFile; + } + + @Override + public NewSymbol newSymbol(int startLine, int startLineOffset, int endLine, int endLineOffset) { + checkInputFileNotNull(); + TextRange declarationRange; + try { + declarationRange = inputFile.newRange(startLine, startLineOffset, endLine, endLineOffset); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to create symbol on file " + inputFile, e); + } + return newSymbol(declarationRange); + } + + @Override + public NewSymbol newSymbol(int startOffset, int endOffset) { + checkInputFileNotNull(); + TextRange declarationRange; + try { + declarationRange = inputFile.newRange(startOffset, endOffset); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to create symbol on file " + inputFile, e); + } + return newSymbol(declarationRange); + } + + @Override + public NewSymbol newSymbol(TextRange range) { + checkInputFileNotNull(); + TreeSet<TextRange> references = new TreeSet<>((o1, o2) -> o1.start().compareTo(o2.start())); + referencesBySymbol.put(range, references); + return new DefaultSymbol(inputFile, range, references); + } + + private static class DefaultSymbol implements NewSymbol { + + private final Collection<TextRange> references; + private final DefaultInputFile inputFile; + private final TextRange declaration; + + public DefaultSymbol(DefaultInputFile inputFile, TextRange declaration, Collection<TextRange> references) { + this.inputFile = inputFile; + this.declaration = declaration; + this.references = references; + } + + @Override + public NewSymbol newReference(int startOffset, int endOffset) { + TextRange referenceRange; + try { + referenceRange = inputFile.newRange(startOffset, endOffset); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to create symbol reference on file " + inputFile, e); + } + return newReference(referenceRange); + } + + @Override + public NewSymbol newReference(int startLine, int startLineOffset, int endLine, int endLineOffset) { + TextRange referenceRange; + try { + referenceRange = inputFile.newRange(startLine, startLineOffset, endLine, endLineOffset); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to create symbol reference on file " + inputFile, e); + } + return newReference(referenceRange); + } + + @Override + public NewSymbol newReference(TextRange range) { + requireNonNull(range, "Provided range is null"); + checkArgument(!declaration.overlap(range), "Overlapping symbol declaration and reference for symbol at %s", declaration); + references.add(range); + return this; + } + + } + + @Override + protected void doSave() { + checkInputFileNotNull(); + storage.store(this); + } + + private void checkInputFileNotNull() { + checkState(inputFile != null, "Call onFile() first"); + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/InMemorySensorStorage.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/InMemorySensorStorage.java new file mode 100644 index 00000000000..26f74db0341 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/InMemorySensorStorage.java @@ -0,0 +1,146 @@ +/* + * 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.impl.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.NewSignificantCode; +import org.sonar.api.batch.sensor.coverage.NewCoverage; +import org.sonar.api.batch.sensor.cpd.NewCpdTokens; +import org.sonar.api.batch.sensor.error.AnalysisError; +import org.sonar.api.batch.sensor.highlighting.NewHighlighting; +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.measure.Measure; +import org.sonar.api.batch.sensor.rule.AdHocRule; +import org.sonar.api.batch.sensor.symbol.NewSymbolTable; + +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, NewHighlighting> 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(AdHocRule adHocRule) { + allAdHocRules.add(adHocRule); + } + + @Override + public void store(NewHighlighting newHighlighting) { + DefaultHighlighting highlighting = (DefaultHighlighting) newHighlighting; + 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(NewCoverage coverage) { + DefaultCoverage defaultCoverage = (DefaultCoverage) coverage; + String fileKey = defaultCoverage.inputFile().key(); + coverageByComponent.computeIfAbsent(fileKey, x -> new ArrayList<>()).add(defaultCoverage); + } + + @Override + public void store(NewCpdTokens cpdTokens) { + DefaultCpdTokens defaultCpdTokens = (DefaultCpdTokens) cpdTokens; + 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(NewSymbolTable newSymbolTable) { + DefaultSymbolTable symbolTable = (DefaultSymbolTable) newSymbolTable; + 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(ExternalIssue issue) { + allExternalIssues.add(issue); + } + + @Override + public void store(NewSignificantCode newSignificantCode) { + DefaultSignificantCode significantCode = (DefaultSignificantCode) newSignificantCode; + 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-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/SensorContextTester.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/SensorContextTester.java new file mode 100644 index 00000000000..6877a771175 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/SensorContextTester.java @@ -0,0 +1,399 @@ +/* + * 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.impl.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.rule.ActiveRules; +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.coverage.NewCoverage; +import org.sonar.api.batch.sensor.cpd.NewCpdTokens; +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.highlighting.NewHighlighting; +import org.sonar.api.batch.sensor.highlighting.TypeOfText; +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.measure.Measure; +import org.sonar.api.batch.sensor.measure.NewMeasure; +import org.sonar.api.batch.sensor.rule.AdHocRule; +import org.sonar.api.batch.sensor.rule.NewAdHocRule; +import org.sonar.api.batch.sensor.symbol.NewSymbolTable; +import org.sonar.api.config.Configuration; +import org.sonar.api.config.Settings; +import org.sonar.api.impl.config.ConfigurationBridge; +import org.sonar.api.impl.config.MapSettings; +import org.sonar.api.impl.fs.DefaultFileSystem; +import org.sonar.api.impl.fs.DefaultInputFile; +import org.sonar.api.impl.fs.DefaultInputModule; +import org.sonar.api.impl.fs.DefaultInputProject; +import org.sonar.api.impl.fs.DefaultTextPointer; +import org.sonar.api.impl.issue.DefaultIssue; +import org.sonar.api.impl.rule.ActiveRulesBuilder; +import org.sonar.api.impl.context.MetadataLoader; +import org.sonar.api.impl.context.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 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 = (DefaultHighlighting) 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); + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/SyntaxHighlightingRule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/SyntaxHighlightingRule.java new file mode 100644 index 00000000000..35d4fbea48a --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/SyntaxHighlightingRule.java @@ -0,0 +1,53 @@ +/* + * 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.impl.sensor; + +import org.apache.commons.lang.builder.ReflectionToStringBuilder; +import org.apache.commons.lang.builder.ToStringStyle; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.batch.sensor.highlighting.TypeOfText; + +public class SyntaxHighlightingRule { + + private final TextRange range; + private final TypeOfText textType; + + private SyntaxHighlightingRule(TextRange range, TypeOfText textType) { + this.range = range; + this.textType = textType; + } + + public static SyntaxHighlightingRule create(TextRange range, TypeOfText textType) { + return new SyntaxHighlightingRule(range, textType); + } + + public TextRange range() { + return range; + } + + public TypeOfText getTextType() { + return textType; + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this, ToStringStyle.SIMPLE_STYLE); + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/package-info.java new file mode 100644 index 00000000000..b304d3197d8 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/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.api.impl.sensor; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultDebtRemediationFunctions.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultDebtRemediationFunctions.java new file mode 100644 index 00000000000..52bf1206057 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultDebtRemediationFunctions.java @@ -0,0 +1,68 @@ +/* + * 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.impl.server; + +import javax.annotation.Nullable; +import org.sonar.api.server.debt.DebtRemediationFunction; +import org.sonar.api.server.debt.internal.DefaultDebtRemediationFunction; +import org.sonar.api.server.rule.RulesDefinition; +import org.sonar.api.utils.MessageException; + +/** + * Factory of {@link org.sonar.api.server.debt.DebtRemediationFunction} that keeps + * a context of rule for better error messages. Used only when declaring rules. + * + * @see org.sonar.api.server.rule.RulesDefinition + */ +class DefaultDebtRemediationFunctions implements RulesDefinition.DebtRemediationFunctions { + + private final String repoKey; + private final String key; + + DefaultDebtRemediationFunctions(String repoKey, String key) { + this.repoKey = repoKey; + this.key = key; + } + + @Override + public DebtRemediationFunction linear(String gapMultiplier) { + return create(DefaultDebtRemediationFunction.Type.LINEAR, gapMultiplier, null); + } + + @Override + public DebtRemediationFunction linearWithOffset(String gapMultiplier, String baseEffort) { + return create(DefaultDebtRemediationFunction.Type.LINEAR_OFFSET, gapMultiplier, baseEffort); + } + + @Override + public DebtRemediationFunction constantPerIssue(String baseEffort) { + return create(DefaultDebtRemediationFunction.Type.CONSTANT_ISSUE, null, baseEffort); + } + + @Override + public DebtRemediationFunction create(DebtRemediationFunction.Type type, @Nullable String gapMultiplier, @Nullable String baseEffort) { + try { + return new DefaultDebtRemediationFunction(type, gapMultiplier, baseEffort); + } catch (Exception e) { + throw MessageException.of(String.format("The rule '%s:%s' is invalid : %s ", this.repoKey, this.key, e.getMessage())); + } + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewParam.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewParam.java new file mode 100644 index 00000000000..6a026db8934 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewParam.java @@ -0,0 +1,85 @@ +/* + * 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.impl.server; + +import javax.annotation.Nullable; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.server.rule.RuleParamType; +import org.sonar.api.server.rule.RulesDefinition; + +import static org.apache.commons.lang.StringUtils.defaultIfEmpty; + +public class DefaultNewParam extends RulesDefinition.NewParam { + private final String key; + private String name; + private String description; + private String defaultValue; + private RuleParamType type = RuleParamType.STRING; + + DefaultNewParam(String key) { + this.key = this.name = key; + } + + @Override + public String key() { + return key; + } + + @Override + public DefaultNewParam setName(@Nullable String s) { + // name must never be null. + this.name = StringUtils.defaultIfBlank(s, key); + return this; + } + + @Override + public DefaultNewParam setType(RuleParamType t) { + this.type = t; + return this; + } + + @Override + public DefaultNewParam setDescription(@Nullable String s) { + this.description = StringUtils.defaultIfBlank(s, null); + return this; + } + + @Override + public DefaultNewParam setDefaultValue(@Nullable String s) { + this.defaultValue = defaultIfEmpty(s, null); + return this; + } + + public String name() { + return name; + } + + public String description() { + return description; + } + + public String defaultValue() { + return defaultValue; + } + + public RuleParamType type() { + return type; + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewRepository.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewRepository.java new file mode 100644 index 00000000000..9fe35843b8c --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewRepository.java @@ -0,0 +1,113 @@ +/* + * 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.impl.server; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.server.rule.RulesDefinition; + +import static org.sonar.api.utils.Preconditions.checkArgument; + +public class DefaultNewRepository implements RulesDefinition.NewRepository { + private final RuleDefinitionContext context; + private final String key; + private final boolean isExternal; + private final String language; + private String name; + private final Map<String, RulesDefinition.NewRule> newRules = new HashMap<>(); + + DefaultNewRepository(RuleDefinitionContext context, String key, String language, boolean isExternal) { + this.context = context; + this.key = key; + this.name = key; + this.language = language; + this.isExternal = isExternal; + } + + @Override + public boolean isExternal() { + return isExternal; + } + + @Override + public String key() { + return key; + } + + String language() { + return language; + } + + Map<String, RulesDefinition.NewRule> newRules() { + return newRules; + } + + String name() { + return name; + } + + @Override + public DefaultNewRepository setName(@Nullable String s) { + if (StringUtils.isNotEmpty(s)) { + this.name = s; + } + return this; + } + + @Override + public RulesDefinition.NewRule createRule(String ruleKey) { + checkArgument(!newRules.containsKey(ruleKey), "The rule '%s' of repository '%s' is declared several times", ruleKey, key); + RulesDefinition.NewRule newRule = new DefaultNewRule(context.currentPluginKey(), key, ruleKey); + newRules.put(ruleKey, newRule); + return newRule; + } + + @CheckForNull + @Override + public RulesDefinition.NewRule rule(String ruleKey) { + return newRules.get(ruleKey); + } + + @Override + public Collection<RulesDefinition.NewRule> rules() { + return newRules.values(); + } + + @Override + public void done() { + // note that some validations can be done here, for example for + // verifying that at least one rule is declared + + context.registerRepository(this); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("NewRepository{"); + sb.append("key='").append(key).append('\''); + sb.append(", language='").append(language).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewRule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewRule.java new file mode 100644 index 00000000000..465b8f97bc2 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewRule.java @@ -0,0 +1,350 @@ +/* + * 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.impl.server; + +import java.io.IOException; +import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.apache.commons.io.IOUtils; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rule.RuleScope; +import org.sonar.api.rule.RuleStatus; +import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; +import org.sonar.api.server.debt.DebtRemediationFunction; +import org.sonar.api.server.rule.RuleTagFormat; +import org.sonar.api.server.rule.RulesDefinition; + +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.commons.lang.StringUtils.isEmpty; +import static org.apache.commons.lang.StringUtils.trimToNull; +import static org.sonar.api.utils.Preconditions.checkArgument; +import static org.sonar.api.utils.Preconditions.checkState; + +class DefaultNewRule extends RulesDefinition.NewRule { + private final String pluginKey; + private final String repoKey; + private final String key; + private RuleType type; + private String name; + private String htmlDescription; + private String markdownDescription; + private String internalKey; + private String severity = Severity.MAJOR; + private boolean template; + private RuleStatus status = RuleStatus.defaultStatus(); + private DebtRemediationFunction debtRemediationFunction; + private String gapDescription; + private final Set<String> tags = new TreeSet<>(); + private final Set<String> securityStandards = new TreeSet<>(); + private final Map<String, RulesDefinition.NewParam> paramsByKey = new HashMap<>(); + private final RulesDefinition.DebtRemediationFunctions functions; + private boolean activatedByDefault; + private RuleScope scope; + private final Set<RuleKey> deprecatedRuleKeys = new TreeSet<>(); + + DefaultNewRule(@Nullable String pluginKey, String repoKey, String key) { + this.pluginKey = pluginKey; + this.repoKey = repoKey; + this.key = key; + this.functions = new DefaultDebtRemediationFunctions(repoKey, key); + } + + @Override + public String key() { + return this.key; + } + + @CheckForNull + @Override + public RuleScope scope() { + return this.scope; + } + + @Override + public DefaultNewRule setScope(RuleScope scope) { + this.scope = scope; + return this; + } + + @Override + public DefaultNewRule setName(String s) { + this.name = trimToNull(s); + return this; + } + + @Override + public DefaultNewRule setTemplate(boolean template) { + this.template = template; + return this; + } + + @Override + public DefaultNewRule setActivatedByDefault(boolean activatedByDefault) { + this.activatedByDefault = activatedByDefault; + return this; + } + + @Override + public DefaultNewRule setSeverity(String s) { + checkArgument(Severity.ALL.contains(s), "Severity of rule %s is not correct: %s", this, s); + this.severity = s; + return this; + } + + @Override + public DefaultNewRule setType(RuleType t) { + this.type = t; + return this; + } + + @Override + public DefaultNewRule setHtmlDescription(@Nullable String s) { + checkState(markdownDescription == null, "Rule '%s' already has a Markdown description", this); + this.htmlDescription = trimToNull(s); + return this; + } + + @Override + public DefaultNewRule setHtmlDescription(@Nullable URL classpathUrl) { + if (classpathUrl != null) { + try { + setHtmlDescription(IOUtils.toString(classpathUrl, UTF_8)); + } catch (IOException e) { + throw new IllegalStateException("Fail to read: " + classpathUrl, e); + } + } else { + this.htmlDescription = null; + } + return this; + } + + @Override + public DefaultNewRule setMarkdownDescription(@Nullable String s) { + checkState(htmlDescription == null, "Rule '%s' already has an HTML description", this); + this.markdownDescription = trimToNull(s); + return this; + } + + @Override + public DefaultNewRule setMarkdownDescription(@Nullable URL classpathUrl) { + if (classpathUrl != null) { + try { + setMarkdownDescription(IOUtils.toString(classpathUrl, UTF_8)); + } catch (IOException e) { + throw new IllegalStateException("Fail to read: " + classpathUrl, e); + } + } else { + this.markdownDescription = null; + } + return this; + } + + @Override + public DefaultNewRule setStatus(RuleStatus status) { + checkArgument(RuleStatus.REMOVED != status, "Status 'REMOVED' is not accepted on rule '%s'", this); + this.status = status; + return this; + } + + @Override + public DefaultNewRule setDebtSubCharacteristic(@Nullable String s) { + return this; + } + + @Override + public RulesDefinition.DebtRemediationFunctions debtRemediationFunctions() { + return functions; + } + + @Override + public DefaultNewRule setDebtRemediationFunction(@Nullable DebtRemediationFunction fn) { + this.debtRemediationFunction = fn; + return this; + } + + @Deprecated + @Override + public DefaultNewRule setEffortToFixDescription(@Nullable String s) { + return setGapDescription(s); + } + + @Override + public DefaultNewRule setGapDescription(@Nullable String s) { + this.gapDescription = s; + return this; + } + + @Override + public RulesDefinition.NewParam createParam(String paramKey) { + checkArgument(!paramsByKey.containsKey(paramKey), "The parameter '%s' is declared several times on the rule %s", paramKey, this); + DefaultNewParam param = new DefaultNewParam(paramKey); + paramsByKey.put(paramKey, param); + return param; + } + + @CheckForNull + @Override + public RulesDefinition.NewParam param(String paramKey) { + return paramsByKey.get(paramKey); + } + + @Override + public Collection<RulesDefinition.NewParam> params() { + return paramsByKey.values(); + } + + @Override + public DefaultNewRule addTags(String... list) { + for (String tag : list) { + RuleTagFormat.validate(tag); + tags.add(tag); + } + return this; + } + + @Override + public DefaultNewRule setTags(String... list) { + tags.clear(); + addTags(list); + return this; + } + + @Override + public DefaultNewRule addOwaspTop10(RulesDefinition.OwaspTop10... standards) { + for (RulesDefinition.OwaspTop10 owaspTop10 : standards) { + String standard = "owaspTop10:" + owaspTop10.name().toLowerCase(Locale.ENGLISH); + securityStandards.add(standard); + } + return this; + } + + @Override + public DefaultNewRule addCwe(int... nums) { + for (int num : nums) { + String standard = "cwe:" + num; + securityStandards.add(standard); + } + return this; + } + + @Override + public DefaultNewRule setInternalKey(@Nullable String s) { + this.internalKey = s; + return this; + } + + void validate() { + if (isEmpty(name)) { + throw new IllegalStateException(format("Name of rule %s is empty", this)); + } + if (isEmpty(htmlDescription) && isEmpty(markdownDescription)) { + throw new IllegalStateException(format("One of HTML description or Markdown description must be defined for rule %s", this)); + } + } + + @Override + public DefaultNewRule addDeprecatedRuleKey(String repository, String key) { + deprecatedRuleKeys.add(RuleKey.of(repository, key)); + return this; + } + + String pluginKey() { + return pluginKey; + } + + String repoKey() { + return repoKey; + } + + RuleType type() { + return type; + } + + String name() { + return name; + } + + String htmlDescription() { + return htmlDescription; + } + + String markdownDescription() { + return markdownDescription; + } + + @CheckForNull + String internalKey() { + return internalKey; + } + + String severity() { + return severity; + } + + boolean template() { + return template; + } + + RuleStatus status() { + return status; + } + + DebtRemediationFunction debtRemediationFunction() { + return debtRemediationFunction; + } + + String gapDescription() { + return gapDescription; + } + + Set<String> tags() { + return tags; + } + + Set<String> securityStandards() { + return securityStandards; + } + + Map<String, RulesDefinition.NewParam> paramsByKey() { + return paramsByKey; + } + + boolean activatedByDefault() { + return activatedByDefault; + } + + Set<RuleKey> deprecatedRuleKeys() { + return deprecatedRuleKeys; + } + + @Override + public String toString() { + return format("[repository=%s, key=%s]", repoKey, key); + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultParam.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultParam.java new file mode 100644 index 00000000000..ea414450fdf --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultParam.java @@ -0,0 +1,87 @@ +/* + * 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.impl.server; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.server.rule.RuleParamType; +import org.sonar.api.server.rule.RulesDefinition; + +@Immutable +public class DefaultParam implements RulesDefinition.Param { + private final String key; + private final String name; + private final String description; + private final String defaultValue; + private final RuleParamType type; + + DefaultParam(DefaultNewParam newParam) { + this.key = newParam.key(); + this.name = newParam.name(); + this.description = newParam.description(); + this.defaultValue = newParam.defaultValue(); + this.type = newParam.type(); + } + + @Override + public String key() { + return key; + } + + @Override + public String name() { + return name; + } + + @Override + @Nullable + public String description() { + return description; + } + + @Override + @Nullable + public String defaultValue() { + return defaultValue; + } + + @Override + public RuleParamType type() { + return type; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RulesDefinition.Param that = (RulesDefinition.Param) o; + return key.equals(that.key()); + } + + @Override + public int hashCode() { + return key.hashCode(); + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultRepository.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultRepository.java new file mode 100644 index 00000000000..8d56b973499 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultRepository.java @@ -0,0 +1,128 @@ +/* + * 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.impl.server; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.server.rule.RulesDefinition; +import org.sonar.api.utils.log.Loggers; + +import static java.lang.String.format; +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableMap; + +@Immutable +class DefaultRepository implements RulesDefinition.Repository { + private final String key; + private final String language; + private final String name; + private final boolean isExternal; + private final Map<String, RulesDefinition.Rule> rulesByKey; + + DefaultRepository(DefaultNewRepository newRepository, @Nullable RulesDefinition.Repository mergeInto) { + this.key = newRepository.key(); + this.language = newRepository.language(); + this.isExternal = newRepository.isExternal(); + Map<String, RulesDefinition.Rule> ruleBuilder = new HashMap<>(); + if (mergeInto != null) { + if (!StringUtils.equals(newRepository.language(), mergeInto.language()) || !StringUtils.equals(newRepository.key(), mergeInto.key())) { + throw new IllegalArgumentException(format("Bug - language and key of the repositories to be merged should be the sames: %s and %s", newRepository, mergeInto)); + } + this.name = StringUtils.defaultIfBlank(mergeInto.name(), newRepository.name()); + for (RulesDefinition.Rule rule : mergeInto.rules()) { + if (!newRepository.key().startsWith("common-") && ruleBuilder.containsKey(rule.key())) { + Loggers.get(getClass()).warn("The rule '{}' of repository '{}' is declared several times", rule.key(), mergeInto.key()); + } + ruleBuilder.put(rule.key(), rule); + } + } else { + this.name = newRepository.name(); + } + for (RulesDefinition.NewRule newRule : newRepository.newRules().values()) { + DefaultNewRule defaultNewRule = (DefaultNewRule) newRule; + defaultNewRule.validate(); + ruleBuilder.put(newRule.key(), new DefaultRule(this, defaultNewRule)); + } + this.rulesByKey = unmodifiableMap(ruleBuilder); + } + + @Override + public String key() { + return key; + } + + @Override + public String language() { + return language; + } + + @Override + public String name() { + return name; + } + + @Override + public boolean isExternal() { + return isExternal; + } + + @Override + @CheckForNull + public RulesDefinition.Rule rule(String ruleKey) { + return rulesByKey.get(ruleKey); + } + + @Override + public List<RulesDefinition.Rule> rules() { + return unmodifiableList(new ArrayList<>(rulesByKey.values())); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultRepository that = (DefaultRepository) o; + return key.equals(that.key); + } + + @Override + public int hashCode() { + return key.hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("Repository{"); + sb.append("key='").append(key).append('\''); + sb.append(", language='").append(language).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultRule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultRule.java new file mode 100644 index 00000000000..d56201d6635 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultRule.java @@ -0,0 +1,238 @@ +/* + * 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.impl.server; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import javax.annotation.CheckForNull; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rule.RuleScope; +import org.sonar.api.rule.RuleStatus; +import org.sonar.api.rules.RuleType; +import org.sonar.api.server.debt.DebtRemediationFunction; +import org.sonar.api.server.rule.RuleTagsToTypeConverter; +import org.sonar.api.server.rule.RulesDefinition; + +import static java.lang.String.format; +import static java.util.Collections.unmodifiableList; + +@Immutable +public class DefaultRule implements RulesDefinition.Rule { + private final String pluginKey; + private final RulesDefinition.Repository repository; + private final String repoKey; + private final String key; + private final String name; + private final RuleType type; + private final String htmlDescription; + private final String markdownDescription; + private final String internalKey; + private final String severity; + private final boolean template; + private final DebtRemediationFunction debtRemediationFunction; + private final String gapDescription; + private final Set<String> tags; + private final Set<String> securityStandards; + private final Map<String, RulesDefinition.Param> params; + private final RuleStatus status; + private final boolean activatedByDefault; + private final RuleScope scope; + private final Set<RuleKey> deprecatedRuleKeys; + + DefaultRule(DefaultRepository repository, DefaultNewRule newRule) { + this.pluginKey = newRule.pluginKey(); + this.repository = repository; + this.repoKey = newRule.repoKey(); + this.key = newRule.key(); + this.name = newRule.name(); + this.htmlDescription = newRule.htmlDescription(); + this.markdownDescription = newRule.markdownDescription(); + this.internalKey = newRule.internalKey(); + this.severity = newRule.severity(); + this.template = newRule.template(); + this.status = newRule.status(); + this.debtRemediationFunction = newRule.debtRemediationFunction(); + this.gapDescription = newRule.gapDescription(); + this.scope = newRule.scope() == null ? RuleScope.MAIN : newRule.scope(); + this.type = newRule.type() == null ? RuleTagsToTypeConverter.convert(newRule.tags()) : newRule.type(); + Set<String> tagsBuilder = new TreeSet<>(newRule.tags()); + tagsBuilder.removeAll(RuleTagsToTypeConverter.RESERVED_TAGS); + this.tags = Collections.unmodifiableSet(tagsBuilder); + this.securityStandards = Collections.unmodifiableSet(new TreeSet<>(newRule.securityStandards())); + Map<String, RulesDefinition.Param> paramsBuilder = new HashMap<>(); + for (RulesDefinition.NewParam newParam : newRule.paramsByKey().values()) { + paramsBuilder.put(newParam.key(), new DefaultParam((DefaultNewParam) newParam)); + } + this.params = Collections.unmodifiableMap(paramsBuilder); + this.activatedByDefault = newRule.activatedByDefault(); + this.deprecatedRuleKeys = Collections.unmodifiableSet(new TreeSet<>(newRule.deprecatedRuleKeys())); + } + + public RulesDefinition.Repository repository() { + return repository; + } + + @Override + @CheckForNull + public String pluginKey() { + return pluginKey; + } + + @Override + public String key() { + return key; + } + + @Override + public String name() { + return name; + } + + @Override + public RuleScope scope() { + return scope; + } + + @Override + public RuleType type() { + return type; + } + + @Override + public String severity() { + return severity; + } + + @Override + @CheckForNull + public String htmlDescription() { + return htmlDescription; + } + + @Override + @CheckForNull + public String markdownDescription() { + return markdownDescription; + } + + @Override + public boolean template() { + return template; + } + + @Override + public boolean activatedByDefault() { + return activatedByDefault; + } + + @Override + public RuleStatus status() { + return status; + } + + @CheckForNull + @Deprecated + @Override + public String debtSubCharacteristic() { + return null; + } + + @CheckForNull + @Override + public DebtRemediationFunction debtRemediationFunction() { + return debtRemediationFunction; + } + + @Deprecated + @CheckForNull + @Override + public String effortToFixDescription() { + return gapDescription(); + } + + @CheckForNull + @Override + public String gapDescription() { + return gapDescription; + } + + @CheckForNull + @Override + public RulesDefinition.Param param(String key) { + return params.get(key); + } + + @Override + public List<RulesDefinition.Param> params() { + return unmodifiableList(new ArrayList<>(params.values())); + } + + @Override + public Set<String> tags() { + return tags; + } + + @Override + public Set<String> securityStandards() { + return securityStandards; + } + + @Override + public Set<RuleKey> deprecatedRuleKeys() { + return deprecatedRuleKeys; + } + + @CheckForNull + @Override + public String internalKey() { + return internalKey; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultRule other = (DefaultRule) o; + return key.equals(other.key) && repoKey.equals(other.repoKey); + } + + @Override + public int hashCode() { + int result = repoKey.hashCode(); + result = 31 * result + key.hashCode(); + return result; + } + + @Override + public String toString() { + return format("[repository=%s, key=%s]", repoKey, key); + } +} + diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/RuleDefinitionContext.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/RuleDefinitionContext.java new file mode 100644 index 00000000000..7d47fc25258 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/RuleDefinitionContext.java @@ -0,0 +1,97 @@ +/* + * 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.impl.server; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.server.rule.RulesDefinition; + +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; +import static org.sonar.api.utils.Preconditions.checkState; + +public class RuleDefinitionContext extends RulesDefinition.Context { + private final Map<String, RulesDefinition.Repository> repositoriesByKey = new HashMap<>(); + private String currentPluginKey; + + @Override + public RulesDefinition.NewRepository createRepository(String key, String language) { + return new DefaultNewRepository(this, key, language, false); + } + + @Override + public RulesDefinition.NewRepository createExternalRepository(String engineId, String language) { + return new DefaultNewRepository(this, RuleKey.EXTERNAL_RULE_REPO_PREFIX + engineId, language, true); + } + + @Override + @Deprecated + public RulesDefinition.NewRepository extendRepository(String key, String language) { + return createRepository(key, language); + } + + @Override + @CheckForNull + public RulesDefinition.Repository repository(String key) { + return repositoriesByKey.get(key); + } + + @Override + public List<RulesDefinition.Repository> repositories() { + return unmodifiableList(new ArrayList<>(repositoriesByKey.values())); + } + + @Override + @Deprecated + public List<RulesDefinition.ExtendedRepository> extendedRepositories(String repositoryKey) { + return emptyList(); + } + + @Override + @Deprecated + public List<RulesDefinition.ExtendedRepository> extendedRepositories() { + return emptyList(); + } + + void registerRepository(DefaultNewRepository newRepository) { + RulesDefinition.Repository existing = repositoriesByKey.get(newRepository.key()); + if (existing != null) { + String existingLanguage = existing.language(); + checkState(existingLanguage.equals(newRepository.language()), + "The rule repository '%s' must not be defined for two different languages: %s and %s", + newRepository.key(), existingLanguage, newRepository.language()); + } + repositoriesByKey.put(newRepository.key(), new DefaultRepository(newRepository, existing)); + } + + public String currentPluginKey() { + return currentPluginKey; + } + + @Override + public void setCurrentPluginKey(@Nullable String pluginKey) { + this.currentPluginKey = pluginKey; + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/package-info.java new file mode 100644 index 00000000000..42efaa610b6 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/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.api.impl.server; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/AlwaysIncreasingSystem2.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/AlwaysIncreasingSystem2.java new file mode 100644 index 00000000000..ab25411b6b2 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/AlwaysIncreasingSystem2.java @@ -0,0 +1,71 @@ +/* + * 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.impl.utils; + +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; +import org.sonar.api.utils.System2; + +import static org.sonar.api.utils.Preconditions.checkArgument; + +/** + * A subclass of {@link System2} which implementation of {@link System2#now()} always return a bigger value than the + * previous returned value. + * <p> + * This class is intended to be used in Unit tests. + * </p> + */ +public class AlwaysIncreasingSystem2 extends System2 { + private final AtomicLong now; + private final long increment; + + private AlwaysIncreasingSystem2(Supplier<Long> initialValueSupplier, long increment) { + checkArgument(increment > 0, "increment must be > 0"); + long initialValue = initialValueSupplier.get(); + checkArgument(initialValue >= 0, "Initial value must be >= 0"); + this.now = new AtomicLong(initialValue); + this.increment = increment; + } + + public AlwaysIncreasingSystem2(long increment) { + this(AlwaysIncreasingSystem2::randomInitialValue, increment); + } + + public AlwaysIncreasingSystem2(long initialValue, int increment) { + this(() -> initialValue, increment); + } + + /** + * Values returned by {@link #now()} will start with a random value and increment by 100. + */ + public AlwaysIncreasingSystem2() { + this(AlwaysIncreasingSystem2::randomInitialValue, 100); + } + + @Override + public long now() { + return now.getAndAdd(increment); + } + + private static long randomInitialValue() { + return (long) Math.abs(new Random().nextInt(2_000_000)); + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/DefaultTempFolder.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/DefaultTempFolder.java new file mode 100644 index 00000000000..369058f7b40 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/DefaultTempFolder.java @@ -0,0 +1,126 @@ +/* + * 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.impl.utils; + +import java.nio.file.FileVisitResult; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import org.apache.commons.io.FileUtils; +import org.sonar.api.utils.TempFolder; + +import javax.annotation.Nullable; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +public class DefaultTempFolder implements TempFolder { + private static final Logger LOG = Loggers.get(DefaultTempFolder.class); + + private final File tempDir; + private final boolean deleteOnExit; + + public DefaultTempFolder(File tempDir) { + this(tempDir, false); + } + + public DefaultTempFolder(File tempDir, boolean deleteOnExit) { + this.tempDir = tempDir; + this.deleteOnExit = deleteOnExit; + } + + @Override + public File newDir() { + return createTempDir(tempDir.toPath()).toFile(); + } + + private static Path createTempDir(Path baseDir) { + try { + return Files.createTempDirectory(baseDir, null); + } catch (IOException e) { + throw new IllegalStateException("Failed to create temp directory", e); + } + } + + @Override + public File newDir(String name) { + File dir = new File(tempDir, name); + try { + FileUtils.forceMkdir(dir); + } catch (IOException e) { + throw new IllegalStateException("Failed to create temp directory - " + dir, e); + } + return dir; + } + + @Override + public File newFile() { + return newFile(null, null); + } + + @Override + public File newFile(@Nullable String prefix, @Nullable String suffix) { + return createTempFile(tempDir.toPath(), prefix, suffix).toFile(); + } + + private static Path createTempFile(Path baseDir, String prefix, String suffix) { + try { + return Files.createTempFile(baseDir, prefix, suffix); + } catch (IOException e) { + throw new IllegalStateException("Failed to create temp file", e); + } + } + + public void clean() { + try { + if (tempDir.exists()) { + Files.walkFileTree(tempDir.toPath(), DeleteRecursivelyFileVisitor.INSTANCE); + } + } catch (IOException e) { + LOG.error("Failed to delete temp folder", e); + } + } + + public void stop() { + if (deleteOnExit) { + clean(); + } + } + + private static final class DeleteRecursivelyFileVisitor extends SimpleFileVisitor<Path> { + public static final DeleteRecursivelyFileVisitor INSTANCE = new DeleteRecursivelyFileVisitor(); + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.deleteIfExists(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.deleteIfExists(dir); + return FileVisitResult.CONTINUE; + } + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/JUnitTempFolder.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/JUnitTempFolder.java new file mode 100644 index 00000000000..a52da93ca7c --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/JUnitTempFolder.java @@ -0,0 +1,108 @@ +/* + * 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.impl.utils; + +import org.apache.commons.lang.StringUtils; +import org.junit.rules.ExternalResource; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.sonar.api.utils.TempFolder; + +import javax.annotation.Nullable; + +import java.io.File; +import java.io.IOException; + +/** + * Implementation of {@link org.sonar.api.utils.TempFolder} to be used + * only in JUnit tests. It wraps {@link org.junit.rules.TemporaryFolder}. + * <br> + * Example: + * <pre> + * public class MyTest { + * @@org.junit.Rule + * public JUnitTempFolder temp = new JUnitTempFolder(); + * + * @@org.junit.Test + * public void myTest() throws Exception { + * File dir = temp.newDir(); + * // ... + * } + * } + * </pre> + * + * @since 5.1 + */ +public class JUnitTempFolder extends ExternalResource implements TempFolder { + + private final TemporaryFolder junit = new TemporaryFolder(); + + @Override + public Statement apply(Statement base, Description description) { + return junit.apply(base, description); + } + + @Override + protected void before() throws Throwable { + junit.create(); + } + + @Override + protected void after() { + junit.delete(); + } + + @Override + public File newDir() { + try { + return junit.newFolder(); + } catch (IOException e) { + throw new IllegalStateException("Fail to create temp dir", e); + } + } + + @Override + public File newDir(String name) { + try { + return junit.newFolder(name); + } catch (IOException e) { + throw new IllegalStateException("Fail to create temp dir", e); + } + } + + @Override + public File newFile() { + try { + return junit.newFile(); + } catch (IOException e) { + throw new IllegalStateException("Fail to create temp file", e); + } + } + + @Override + public File newFile(@Nullable String prefix, @Nullable String suffix) { + try { + return junit.newFile(StringUtils.defaultString(prefix) + "-" + StringUtils.defaultString(suffix)); + } catch (IOException e) { + throw new IllegalStateException("Fail to create temp file", e); + } + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/ScannerUtils.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/ScannerUtils.java new file mode 100644 index 00000000000..f23bfbac928 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/ScannerUtils.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.api.impl.utils; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import javax.annotation.Nullable; +import org.apache.commons.lang.StringUtils; + +public class ScannerUtils { + + private ScannerUtils() { + } + + /** + * Clean provided string to remove chars that are not valid as file name. + * + * @param projectKey e.g. my:file + */ + public static String cleanKeyForFilename(String projectKey) { + String cleanKey = StringUtils.deleteWhitespace(projectKey); + return StringUtils.replace(cleanKey, ":", "_"); + } + + public static String encodeForUrl(@Nullable String url) { + try { + return URLEncoder.encode(url == null ? "" : url, StandardCharsets.UTF_8.name()); + + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("Encoding not supported", e); + } + } + + public static String describe(Object o) { + try { + if (o.getClass().getMethod("toString").getDeclaringClass() != Object.class) { + String str = o.toString(); + if (str != null) { + return str; + } + } + } catch (Exception e) { + // fallback + } + + return o.getClass().getName(); + } + + public static String pluralize(String str, int i) { + if (i == 1) { + return str; + } + return str + "s"; + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/TestSystem2.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/TestSystem2.java new file mode 100644 index 00000000000..b1005d5e186 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/TestSystem2.java @@ -0,0 +1,52 @@ +/* + * 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.impl.utils; + +import java.util.TimeZone; +import org.sonar.api.utils.System2; + +public class TestSystem2 extends System2 { + + private long now = 0L; + private TimeZone defaultTimeZone = getDefaultTimeZone(); + + public TestSystem2 setNow(long l) { + this.now = l; + return this; + } + + @Override + public long now() { + if (now <= 0L) { + throw new IllegalStateException("Method setNow() was not called by test"); + } + return now; + } + + public TestSystem2 setDefaultTimeZone(TimeZone defaultTimeZone) { + this.defaultTimeZone = defaultTimeZone; + return this; + } + + @Override + public TimeZone getDefaultTimeZone() { + return defaultTimeZone; + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/WorkDuration.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/WorkDuration.java new file mode 100644 index 00000000000..20349ba155c --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/WorkDuration.java @@ -0,0 +1,194 @@ +/* + * 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.impl.utils; + +import java.io.Serializable; +import javax.annotation.Nullable; +import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang.builder.ToStringStyle; + +/** + * @since 4.2 + */ +public class WorkDuration implements Serializable { + + static final int DAY_POSITION_IN_LONG = 10_000; + static final int HOUR_POSITION_IN_LONG = 100; + static final int MINUTE_POSITION_IN_LONG = 1; + + public enum UNIT { + DAYS, HOURS, MINUTES + } + + private int hoursInDay; + + private long durationInMinutes; + private int days; + private int hours; + private int minutes; + + private WorkDuration(long durationInMinutes, int days, int hours, int minutes, int hoursInDay) { + this.durationInMinutes = durationInMinutes; + this.days = days; + this.hours = hours; + this.minutes = minutes; + this.hoursInDay = hoursInDay; + } + + public static WorkDuration create(int days, int hours, int minutes, int hoursInDay) { + long durationInSeconds = 60L * days * hoursInDay; + durationInSeconds += 60L * hours; + durationInSeconds += minutes; + return new WorkDuration(durationInSeconds, days, hours, minutes, hoursInDay); + } + + public static WorkDuration createFromValueAndUnit(int value, UNIT unit, int hoursInDay) { + switch (unit) { + case DAYS: + return create(value, 0, 0, hoursInDay); + case HOURS: + return create(0, value, 0, hoursInDay); + case MINUTES: + return create(0, 0, value, hoursInDay); + default: + throw new IllegalStateException("Cannot create work duration"); + } + } + + static WorkDuration createFromLong(long duration, int hoursInDay) { + int days = 0; + int hours = 0; + int minutes = 0; + + long time = duration; + Long currentTime = time / WorkDuration.DAY_POSITION_IN_LONG; + if (currentTime > 0) { + days = currentTime.intValue(); + time = time - (currentTime * WorkDuration.DAY_POSITION_IN_LONG); + } + + currentTime = time / WorkDuration.HOUR_POSITION_IN_LONG; + if (currentTime > 0) { + hours = currentTime.intValue(); + time = time - (currentTime * WorkDuration.HOUR_POSITION_IN_LONG); + } + + currentTime = time / WorkDuration.MINUTE_POSITION_IN_LONG; + if (currentTime > 0) { + minutes = currentTime.intValue(); + } + return WorkDuration.create(days, hours, minutes, hoursInDay); + } + + static WorkDuration createFromMinutes(long duration, int hoursInDay) { + int days = (int)(duration / (double)hoursInDay / 60.0); + Long currentDurationInMinutes = duration - (60L * days * hoursInDay); + int hours = (int)(currentDurationInMinutes / 60.0); + currentDurationInMinutes = currentDurationInMinutes - (60L * hours); + return new WorkDuration(duration, days, hours, currentDurationInMinutes.intValue(), hoursInDay); + } + + /** + * Return the duration in number of working days. + * For instance, 3 days and 4 hours will return 3.5 days (if hoursIndDay is 8). + */ + public double toWorkingDays() { + return durationInMinutes / 60d / hoursInDay; + } + + /** + * Return the duration using the following format DDHHMM, where DD is the number of days, HH is the number of months, and MM the number of minutes. + * For instance, 3 days and 4 hours will return 030400 (if hoursIndDay is 8). + */ + public long toLong() { + int workingDays = days; + int workingHours = hours; + if (hours >= hoursInDay) { + int nbAdditionalDays = hours / hoursInDay; + workingDays += nbAdditionalDays; + workingHours = hours - (nbAdditionalDays * hoursInDay); + } + return 1L * workingDays * DAY_POSITION_IN_LONG + workingHours * HOUR_POSITION_IN_LONG + minutes * MINUTE_POSITION_IN_LONG; + } + + public long toMinutes() { + return durationInMinutes; + } + + public WorkDuration add(@Nullable WorkDuration with) { + if (with != null) { + return WorkDuration.createFromMinutes(this.toMinutes() + with.toMinutes(), this.hoursInDay); + } else { + return this; + } + } + + public WorkDuration subtract(@Nullable WorkDuration with) { + if (with != null) { + return WorkDuration.createFromMinutes(this.toMinutes() - with.toMinutes(), this.hoursInDay); + } else { + return this; + } + } + + public WorkDuration multiply(int factor) { + return WorkDuration.createFromMinutes(this.toMinutes() * factor, this.hoursInDay); + } + + public int days() { + return days; + } + + public int hours() { + return hours; + } + + public int minutes() { + return minutes; + } + + int hoursInDay() { + return hoursInDay; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + WorkDuration that = (WorkDuration) o; + return durationInMinutes == that.durationInMinutes; + + } + + @Override + public int hashCode() { + return (int) (durationInMinutes ^ (durationInMinutes >>> 32)); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/package-info.java new file mode 100644 index 00000000000..335c370e59a --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/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.api.impl.utils; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/PartImpl.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/PartImpl.java new file mode 100644 index 00000000000..ed161265484 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/PartImpl.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.api.impl.ws; + +import java.io.InputStream; +import org.sonar.api.server.ws.Request; + +public class PartImpl implements Request.Part { + + private final InputStream inputStream; + private final String fileName; + + public PartImpl(InputStream inputStream, String fileName) { + this.inputStream = inputStream; + this.fileName = fileName; + } + + @Override + public InputStream getInputStream() { + return inputStream; + } + + @Override + public String getFileName() { + return fileName; + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/SimpleGetRequest.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/SimpleGetRequest.java new file mode 100644 index 00000000000..a1135669d53 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/SimpleGetRequest.java @@ -0,0 +1,148 @@ +/* + * 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.impl.ws; + +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.apache.commons.io.IOUtils; +import org.sonar.api.server.ws.LocalConnector; +import org.sonar.api.server.ws.Request; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; + +/** + * Fake implementation of {@link org.sonar.api.server.ws.Request} used + * for testing. Call the method {@link #setParam(String, String)} to + * emulate some parameter values. + */ +public class SimpleGetRequest extends Request { + + private final Map<String, String[]> params = new HashMap<>(); + private final Map<String, Part> parts = new HashMap<>(); + private final Map<String, String> headers = new HashMap<>(); + private String mediaType = "application/json"; + private String path; + + @Override + public String method() { + return "GET"; + } + + @Override + public String getMediaType() { + return mediaType; + } + + public SimpleGetRequest setMediaType(String mediaType) { + requireNonNull(mediaType); + this.mediaType = mediaType; + return this; + } + + @Override + public boolean hasParam(String key) { + return params.keySet().contains(key); + } + + @Override + public String param(String key) { + String[] strings = params.get(key); + return strings == null || strings.length == 0 ? null : strings[0]; + } + + @Override + public List<String> multiParam(String key) { + String value = param(key); + return value == null ? emptyList() : singletonList(value); + } + + @Override + @CheckForNull + public List<String> paramAsStrings(String key) { + String value = param(key); + if (value == null) { + return null; + } + + return Arrays.stream(value.split(",")).map(String::trim).filter(x -> !x.isEmpty()).collect(Collectors.toList()); + } + + @Override + public InputStream paramAsInputStream(String key) { + return IOUtils.toInputStream(param(key), UTF_8); + } + + public SimpleGetRequest setParam(String key, @Nullable String value) { + if (value != null) { + params.put(key, new String[] {value}); + } + return this; + } + + @Override + public Map<String, String[]> getParams() { + return params; + } + + @Override + public Part paramAsPart(String key) { + return parts.get(key); + } + + public SimpleGetRequest setPart(String key, InputStream input, String fileName) { + parts.put(key, new PartImpl(input, fileName)); + return this; + } + + @Override + public LocalConnector localConnector() { + throw new UnsupportedOperationException(); + } + + @Override + public String getPath() { + return path; + } + + public SimpleGetRequest setPath(String path) { + this.path = path; + return this; + } + + @Override + public Optional<String> header(String name) { + return Optional.ofNullable(headers.get(name)); + } + + public SimpleGetRequest setHeader(String name, String value) { + headers.put(name, value); + return this; + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/ValidatingRequest.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/ValidatingRequest.java new file mode 100644 index 00000000000..6e7d90b6a06 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/ValidatingRequest.java @@ -0,0 +1,242 @@ +/* + * 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.impl.ws; + +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.server.ws.LocalConnector; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.WebService; + +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang.StringUtils.defaultString; +import static org.sonar.api.utils.Preconditions.checkArgument; + +/** + * @since 4.2 + */ +public abstract class ValidatingRequest extends Request { + + private static final String COMMA_SPLITTER = ","; + private WebService.Action action; + private LocalConnector localConnector; + + public void setAction(WebService.Action action) { + this.action = action; + } + + public WebService.Action action() { + return action; + } + + @Override + public LocalConnector localConnector() { + requireNonNull(localConnector, "Local connector has not been set"); + return localConnector; + } + + public void setLocalConnector(LocalConnector lc) { + this.localConnector = lc; + } + + @Override + @CheckForNull + public String param(String key) { + WebService.Param definition = action.param(key); + String rawValue = readParam(key, definition); + String rawValueOrDefault = defaultString(rawValue, definition.defaultValue()); + String value = rawValueOrDefault == null ? null : trim(rawValueOrDefault); + validateRequiredValue(key, definition, rawValue); + if (value == null) { + return null; + } + validatePossibleValues(key, value, definition); + validateMaximumLength(key, definition, rawValueOrDefault); + validateMinimumLength(key, definition, rawValueOrDefault); + validateMaximumValue(key, definition, value); + return value; + } + + @Override + public List<String> multiParam(String key) { + WebService.Param definition = action.param(key); + List<String> values = readMultiParamOrDefaultValue(key, definition); + return validateValues(values, definition); + } + + private static String trim(String s) { + int begin; + for (begin = 0; begin < s.length(); begin++) { + if (!Character.isWhitespace(s.charAt(begin))) { + break; + } + } + + int end; + for (end = s.length(); end > begin; end--) { + if (!Character.isWhitespace(s.charAt(end - 1))) { + break; + } + } + return s.substring(begin, end); + } + + @Override + @CheckForNull + public InputStream paramAsInputStream(String key) { + return readInputStreamParam(key); + } + + @Override + @CheckForNull + public Part paramAsPart(String key) { + return readPart(key); + } + + @CheckForNull + @Override + public List<String> paramAsStrings(String key) { + WebService.Param definition = action.param(key); + String value = defaultString(readParam(key, definition), definition.defaultValue()); + if (value == null) { + return null; + } + List<String> values = Arrays.stream(value.split(COMMA_SPLITTER)) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + return validateValues(values, definition); + } + + @CheckForNull + @Override + public <E extends Enum<E>> List<E> paramAsEnums(String key, Class<E> enumClass) { + List<String> values = paramAsStrings(key); + if (values == null) { + return null; + } + return values.stream() + .filter(s -> !s.isEmpty()) + .map(value -> Enum.valueOf(enumClass, value)) + .collect(Collectors.toList()); + } + + @CheckForNull + private String readParam(String key, @Nullable WebService.Param definition) { + checkArgument(definition != null, "BUG - parameter '%s' is undefined for action '%s'", key, action.key()); + String deprecatedKey = definition.deprecatedKey(); + return deprecatedKey != null ? defaultString(readParam(deprecatedKey), readParam(key)) : readParam(key); + } + + private List<String> readMultiParamOrDefaultValue(String key, @Nullable WebService.Param definition) { + checkArgument(definition != null, "BUG - parameter '%s' is undefined for action '%s'", key, action.key()); + + List<String> keyValues = readMultiParam(key); + if (!keyValues.isEmpty()) { + return keyValues; + } + + String deprecatedKey = definition.deprecatedKey(); + List<String> deprecatedKeyValues = deprecatedKey == null ? emptyList() : readMultiParam(deprecatedKey); + if (!deprecatedKeyValues.isEmpty()) { + return deprecatedKeyValues; + } + + String defaultValue = definition.defaultValue(); + return defaultValue == null ? emptyList() : singletonList(defaultValue); + } + + @CheckForNull + protected abstract String readParam(String key); + + protected abstract List<String> readMultiParam(String key); + + @CheckForNull + protected abstract InputStream readInputStreamParam(String key); + + @CheckForNull + protected abstract Part readPart(String key); + + private static List<String> validateValues(List<String> values, WebService.Param definition) { + Integer maximumValues = definition.maxValuesAllowed(); + checkArgument(maximumValues == null || values.size() <= maximumValues, "'%s' can contains only %s values, got %s", definition.key(), maximumValues, values.size()); + values.forEach(value -> validatePossibleValues(definition.key(), value, definition)); + return values; + } + + private static void validatePossibleValues(String key, String value, WebService.Param definition) { + Set<String> possibleValues = definition.possibleValues(); + if (possibleValues == null) { + return; + } + checkArgument(possibleValues.contains(value), "Value of parameter '%s' (%s) must be one of: %s", key, value, possibleValues); + } + + private static void validateMaximumLength(String key, WebService.Param definition, String valueOrDefault) { + Integer maximumLength = definition.maximumLength(); + if (maximumLength == null) { + return; + } + int valueLength = valueOrDefault.length(); + checkArgument(valueLength <= maximumLength, "'%s' length (%s) is longer than the maximum authorized (%s)", key, valueLength, maximumLength); + } + + private static void validateMinimumLength(String key, WebService.Param definition, String valueOrDefault) { + Integer minimumLength = definition.minimumLength(); + if (minimumLength == null) { + return; + } + int valueLength = valueOrDefault.length(); + checkArgument(valueLength >= minimumLength, "'%s' length (%s) is shorter than the minimum authorized (%s)", key, valueLength, minimumLength); + } + + private static void validateMaximumValue(String key, WebService.Param definition, String value) { + Integer maximumValue = definition.maximumValue(); + if (maximumValue == null) { + return; + } + int valueAsInt = validateAsNumeric(key, value); + checkArgument(valueAsInt <= maximumValue, "'%s' value (%s) must be less than %s", key, valueAsInt, maximumValue); + } + + private static void validateRequiredValue(String key, WebService.Param definition, String value) { + boolean required = definition.isRequired(); + if (required) { + checkArgument(value != null, format(MSG_PARAMETER_MISSING, key)); + } + } + + private static int validateAsNumeric(String key, String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException exception) { + throw new IllegalArgumentException(format("'%s' value '%s' cannot be parsed as an integer", key, value), exception); + } + } + +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/package-info.java new file mode 100644 index 00000000000..306c7a7c165 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/package-info.java @@ -0,0 +1,23 @@ +/* + * 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.api.impl.ws; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/config/MapSettingsTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/config/MapSettingsTest.java new file mode 100644 index 00000000000..922d767dfac --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/config/MapSettingsTest.java @@ -0,0 +1,566 @@ +/* + * 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.impl.config; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Random; +import java.util.function.BiConsumer; +import java.util.stream.IntStream; +import org.assertj.core.data.Offset; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.sonar.api.Properties; +import org.sonar.api.Property; +import org.sonar.api.PropertyType; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.api.config.Settings; +import org.sonar.api.impl.config.MapSettings; +import org.sonar.api.utils.DateUtils; + +import static java.util.Collections.singletonList; +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(DataProviderRunner.class) +public class MapSettingsTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private PropertyDefinitions definitions; + + @Properties({ + @Property(key = "hello", name = "Hello", defaultValue = "world"), + @Property(key = "date", name = "Date", defaultValue = "2010-05-18"), + @Property(key = "datetime", name = "DateTime", defaultValue = "2010-05-18T15:50:45+0100"), + @Property(key = "boolean", name = "Boolean", defaultValue = "true"), + @Property(key = "falseboolean", name = "False Boolean", defaultValue = "false"), + @Property(key = "integer", name = "Integer", defaultValue = "12345"), + @Property(key = "array", name = "Array", defaultValue = "one,two,three"), + @Property(key = "multi_values", name = "Array", defaultValue = "1,2,3", multiValues = true), + @Property(key = "sonar.jira", name = "Jira Server", type = PropertyType.PROPERTY_SET, propertySetKey = "jira"), + @Property(key = "newKey", name = "New key", deprecatedKey = "oldKey"), + @Property(key = "newKeyWithDefaultValue", name = "New key with default value", deprecatedKey = "oldKeyWithDefaultValue", defaultValue = "default_value"), + @Property(key = "new_multi_values", name = "New multi values", defaultValue = "1,2,3", multiValues = true, deprecatedKey = "old_multi_values") + }) + private static class Init { + } + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void init_definitions() { + definitions = new PropertyDefinitions(); + definitions.addComponent(Init.class); + } + + @Test + public void set_throws_NPE_if_key_is_null() { + org.sonar.api.impl.config.MapSettings underTest = new org.sonar.api.impl.config.MapSettings(); + + expectKeyNullNPE(); + + underTest.set(null, randomAlphanumeric(3)); + } + + @Test + public void set_throws_NPE_if_value_is_null() { + org.sonar.api.impl.config.MapSettings underTest = new org.sonar.api.impl.config.MapSettings(); + + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("value can't be null"); + + underTest.set(randomAlphanumeric(3), null); + } + + @Test + public void set_accepts_empty_value_and_trims_it() { + org.sonar.api.impl.config.MapSettings underTest = new org.sonar.api.impl.config.MapSettings(); + Random random = new Random(); + String key = randomAlphanumeric(3); + + underTest.set(key, blank(random)); + + assertThat(underTest.getString(key)).isEmpty(); + } + + @Test + public void default_values_should_be_loaded_from_definitions() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + assertThat(settings.getDefaultValue("hello")).isEqualTo("world"); + } + + @Test + @UseDataProvider("setPropertyCalls") + public void all_setProperty_methods_throws_NPE_if_key_is_null(BiConsumer<Settings, String> setPropertyCaller) { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + + expectKeyNullNPE(); + + setPropertyCaller.accept(settings, null); + } + + @Test + public void set_property_string_throws_NPE_if_key_is_null() { + String key = randomAlphanumeric(3); + + Settings underTest = new org.sonar.api.impl.config.MapSettings(new PropertyDefinitions(singletonList(PropertyDefinition.builder(key).multiValues(true).build()))); + + expectKeyNullNPE(); + + underTest.setProperty(null, new String[] {"1", "2"}); + } + + private void expectKeyNullNPE() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("key can't be null"); + } + + @Test + @UseDataProvider("setPropertyCalls") + public void all_set_property_methods_trims_key(BiConsumer<Settings, String> setPropertyCaller) { + Settings underTest = new org.sonar.api.impl.config.MapSettings(); + + Random random = new Random(); + String blankBefore = blank(random); + String blankAfter = blank(random); + String key = randomAlphanumeric(3); + + setPropertyCaller.accept(underTest, blankBefore + key + blankAfter); + + assertThat(underTest.hasKey(key)).isTrue(); + } + + @Test + public void set_property_string_array_trims_key() { + String key = randomAlphanumeric(3); + + Settings underTest = new org.sonar.api.impl.config.MapSettings(new PropertyDefinitions(singletonList(PropertyDefinition.builder(key).multiValues(true).build()))); + + Random random = new Random(); + String blankBefore = blank(random); + String blankAfter = blank(random); + + underTest.setProperty(blankBefore + key + blankAfter, new String[] {"1", "2"}); + + assertThat(underTest.hasKey(key)).isTrue(); + } + + private static String blank(Random random) { + StringBuilder b = new StringBuilder(); + IntStream.range(0, random.nextInt(3)).mapToObj(s -> " ").forEach(b::append); + return b.toString(); + } + + @DataProvider + public static Object[][] setPropertyCalls() { + List<BiConsumer<Settings, String>> callers = Arrays.asList( + (settings, key) -> settings.setProperty(key, 123), + (settings, key) -> settings.setProperty(key, 123L), + (settings, key) -> settings.setProperty(key, 123.2F), + (settings, key) -> settings.setProperty(key, 123.2D), + (settings, key) -> settings.setProperty(key, false), + (settings, key) -> settings.setProperty(key, new Date()), + (settings, key) -> settings.setProperty(key, new Date(), true)); + + return callers.stream().map(t -> new Object[] {t}).toArray(Object[][]::new); + } + + @Test + public void setProperty_methods_trims_value() { + Settings underTest = new org.sonar.api.impl.config.MapSettings(); + + Random random = new Random(); + String blankBefore = blank(random); + String blankAfter = blank(random); + String key = randomAlphanumeric(3); + String value = randomAlphanumeric(3); + + underTest.setProperty(key, blankBefore + value + blankAfter); + + assertThat(underTest.getString(key)).isEqualTo(value); + } + + @Test + public void set_property_int() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("foo", 123); + assertThat(settings.getInt("foo")).isEqualTo(123); + assertThat(settings.getString("foo")).isEqualTo("123"); + assertThat(settings.getBoolean("foo")).isFalse(); + } + + @Test + public void default_number_values_are_zero() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + assertThat(settings.getInt("foo")).isEqualTo(0); + assertThat(settings.getLong("foo")).isEqualTo(0L); + } + + @Test + public void getInt_value_must_be_valid() { + thrown.expect(NumberFormatException.class); + + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("foo", "not a number"); + settings.getInt("foo"); + } + + @Test + public void all_values_should_be_trimmed_set_property() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("foo", " FOO "); + assertThat(settings.getString("foo")).isEqualTo("FOO"); + } + + @Test + public void test_get_default_value() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + assertThat(settings.getDefaultValue("unknown")).isNull(); + } + + @Test + public void test_get_string() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + settings.setProperty("hello", "Russia"); + assertThat(settings.getString("hello")).isEqualTo("Russia"); + } + + @Test + public void setProperty_date() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + Date date = DateUtils.parseDateTime("2010-05-18T15:50:45+0100"); + settings.setProperty("aDate", date); + settings.setProperty("aDateTime", date, true); + + assertThat(settings.getString("aDate")).isEqualTo("2010-05-18"); + assertThat(settings.getString("aDateTime")).startsWith("2010-05-18T"); + } + + @Test + public void test_get_date() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + assertThat(settings.getDate("unknown")).isNull(); + assertThat(settings.getDate("date").getDate()).isEqualTo(18); + assertThat(settings.getDate("date").getMonth()).isEqualTo(4); + } + + @Test + public void test_get_date_not_found() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + assertThat(settings.getDate("unknown")).isNull(); + } + + @Test + public void test_get_datetime() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + assertThat(settings.getDateTime("unknown")).isNull(); + assertThat(settings.getDateTime("datetime").getDate()).isEqualTo(18); + assertThat(settings.getDateTime("datetime").getMonth()).isEqualTo(4); + assertThat(settings.getDateTime("datetime").getMinutes()).isEqualTo(50); + } + + @Test + public void test_get_double() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("from_double", 3.14159); + settings.setProperty("from_string", "3.14159"); + assertThat(settings.getDouble("from_double")).isEqualTo(3.14159, Offset.offset(0.00001)); + assertThat(settings.getDouble("from_string")).isEqualTo(3.14159, Offset.offset(0.00001)); + assertThat(settings.getDouble("unknown")).isNull(); + } + + @Test + public void test_get_float() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("from_float", 3.14159f); + settings.setProperty("from_string", "3.14159"); + assertThat(settings.getDouble("from_float")).isEqualTo(3.14159f, Offset.offset(0.00001)); + assertThat(settings.getDouble("from_string")).isEqualTo(3.14159f, Offset.offset(0.00001)); + assertThat(settings.getDouble("unknown")).isNull(); + } + + @Test + public void test_get_bad_float() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("foo", "bar"); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage("The property 'foo' is not a float value"); + settings.getFloat("foo"); + } + + @Test + public void test_get_bad_double() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("foo", "bar"); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage("The property 'foo' is not a double value"); + settings.getDouble("foo"); + } + + @Test + public void testSetNullFloat() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("foo", (Float) null); + assertThat(settings.getFloat("foo")).isNull(); + } + + @Test + public void testSetNullDouble() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("foo", (Double) null); + assertThat(settings.getDouble("foo")).isNull(); + } + + @Test + public void getStringArray() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + String[] array = settings.getStringArray("array"); + assertThat(array).isEqualTo(new String[] {"one", "two", "three"}); + } + + @Test + public void setStringArray() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + settings.setProperty("multi_values", new String[] {"A", "B"}); + String[] array = settings.getStringArray("multi_values"); + assertThat(array).isEqualTo(new String[] {"A", "B"}); + } + + @Test + public void setStringArrayTrimValues() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + settings.setProperty("multi_values", new String[] {" A ", " B "}); + String[] array = settings.getStringArray("multi_values"); + assertThat(array).isEqualTo(new String[] {"A", "B"}); + } + + @Test + public void setStringArrayEscapeCommas() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + settings.setProperty("multi_values", new String[] {"A,B", "C,D"}); + String[] array = settings.getStringArray("multi_values"); + assertThat(array).isEqualTo(new String[] {"A,B", "C,D"}); + } + + @Test + public void setStringArrayWithEmptyValues() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + settings.setProperty("multi_values", new String[] {"A,B", "", "C,D"}); + String[] array = settings.getStringArray("multi_values"); + assertThat(array).isEqualTo(new String[] {"A,B", "", "C,D"}); + } + + @Test + public void setStringArrayWithNullValues() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + settings.setProperty("multi_values", new String[] {"A,B", null, "C,D"}); + String[] array = settings.getStringArray("multi_values"); + assertThat(array).isEqualTo(new String[] {"A,B", "", "C,D"}); + } + + @Test(expected = IllegalStateException.class) + public void shouldFailToSetArrayValueOnSingleValueProperty() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + settings.setProperty("array", new String[] {"A", "B", "C"}); + } + + @Test + public void getStringArray_no_value() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + String[] array = settings.getStringArray("array"); + assertThat(array).isEmpty(); + } + + @Test + public void shouldTrimArray() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("foo", " one, two, three "); + String[] array = settings.getStringArray("foo"); + assertThat(array).isEqualTo(new String[] {"one", "two", "three"}); + } + + @Test + public void shouldKeepEmptyValuesWhenSplitting() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("foo", " one, , two"); + String[] array = settings.getStringArray("foo"); + assertThat(array).isEqualTo(new String[] {"one", "", "two"}); + } + + @Test + public void testDefaultValueOfGetString() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + assertThat(settings.getString("hello")).isEqualTo("world"); + } + + @Test + public void set_property_boolean() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("foo", true); + settings.setProperty("bar", false); + assertThat(settings.getBoolean("foo")).isTrue(); + assertThat(settings.getBoolean("bar")).isFalse(); + assertThat(settings.getString("foo")).isEqualTo("true"); + assertThat(settings.getString("bar")).isEqualTo("false"); + } + + @Test + public void ignore_case_of_boolean_values() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("foo", "true"); + settings.setProperty("bar", "TRUE"); + // labels in UI + settings.setProperty("baz", "True"); + + assertThat(settings.getBoolean("foo")).isTrue(); + assertThat(settings.getBoolean("bar")).isTrue(); + assertThat(settings.getBoolean("baz")).isTrue(); + } + + @Test + public void get_boolean() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + assertThat(settings.getBoolean("boolean")).isTrue(); + assertThat(settings.getBoolean("falseboolean")).isFalse(); + assertThat(settings.getBoolean("unknown")).isFalse(); + assertThat(settings.getBoolean("hello")).isFalse(); + } + + @Test + public void shouldCreateByIntrospectingComponent() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.getDefinitions().addComponent(MyComponent.class); + + // property definition has been loaded, ie for default value + assertThat(settings.getDefaultValue("foo")).isEqualTo("bar"); + } + + @Property(key = "foo", name = "Foo", defaultValue = "bar") + public static class MyComponent { + + } + + @Test + public void getStringLines_no_value() { + assertThat(new org.sonar.api.impl.config.MapSettings().getStringLines("foo")).hasSize(0); + } + + @Test + public void getStringLines_single_line() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("foo", "the line"); + assertThat(settings.getStringLines("foo")).isEqualTo(new String[] {"the line"}); + } + + @Test + public void getStringLines_linux() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("foo", "one\ntwo"); + assertThat(settings.getStringLines("foo")).isEqualTo(new String[] {"one", "two"}); + + settings.setProperty("foo", "one\ntwo\n"); + assertThat(settings.getStringLines("foo")).isEqualTo(new String[] {"one", "two"}); + } + + @Test + public void getStringLines_windows() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("foo", "one\r\ntwo"); + assertThat(settings.getStringLines("foo")).isEqualTo(new String[] {"one", "two"}); + + settings.setProperty("foo", "one\r\ntwo\r\n"); + assertThat(settings.getStringLines("foo")).isEqualTo(new String[] {"one", "two"}); + } + + @Test + public void getStringLines_mix() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("foo", "one\r\ntwo\nthree"); + assertThat(settings.getStringLines("foo")).isEqualTo(new String[] {"one", "two", "three"}); + } + + @Test + public void getKeysStartingWith() { + Settings settings = new org.sonar.api.impl.config.MapSettings(); + settings.setProperty("sonar.jdbc.url", "foo"); + settings.setProperty("sonar.jdbc.username", "bar"); + settings.setProperty("sonar.security", "admin"); + + assertThat(settings.getKeysStartingWith("sonar")).containsOnly("sonar.jdbc.url", "sonar.jdbc.username", "sonar.security"); + assertThat(settings.getKeysStartingWith("sonar.jdbc")).containsOnly("sonar.jdbc.url", "sonar.jdbc.username"); + assertThat(settings.getKeysStartingWith("other")).hasSize(0); + } + + @Test + public void should_fallback_deprecated_key_to_default_value_of_new_key() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + + assertThat(settings.getString("newKeyWithDefaultValue")).isEqualTo("default_value"); + assertThat(settings.getString("oldKeyWithDefaultValue")).isEqualTo("default_value"); + } + + @Test + public void should_fallback_deprecated_key_to_new_key() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + settings.setProperty("newKey", "value of newKey"); + + assertThat(settings.getString("newKey")).isEqualTo("value of newKey"); + assertThat(settings.getString("oldKey")).isEqualTo("value of newKey"); + } + + @Test + public void should_load_value_of_deprecated_key() { + // it's used for example when deprecated settings are set through command-line + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + settings.setProperty("oldKey", "value of oldKey"); + + assertThat(settings.getString("newKey")).isEqualTo("value of oldKey"); + assertThat(settings.getString("oldKey")).isEqualTo("value of oldKey"); + } + + @Test + public void should_load_values_of_deprecated_key() { + Settings settings = new org.sonar.api.impl.config.MapSettings(definitions); + settings.setProperty("oldKey", "a,b"); + + assertThat(settings.getStringArray("newKey")).containsOnly("a", "b"); + assertThat(settings.getStringArray("oldKey")).containsOnly("a", "b"); + } + + @Test + public void should_support_deprecated_props_with_multi_values() { + Settings settings = new MapSettings(definitions); + settings.setProperty("new_multi_values", new String[] {" A ", " B "}); + assertThat(settings.getStringArray("new_multi_values")).isEqualTo(new String[] {"A", "B"}); + assertThat(settings.getStringArray("old_multi_values")).isEqualTo(new String[] {"A", "B"}); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/config/MultivaluePropertyTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/config/MultivaluePropertyTest.java new file mode 100644 index 00000000000..63e3c442cfd --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/config/MultivaluePropertyTest.java @@ -0,0 +1,265 @@ +/* + * 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.impl.config; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Random; +import java.util.function.Function; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.impl.config.MultivalueProperty.parseAsCsv; +import static org.sonar.api.impl.config.MultivalueProperty.trimFieldsAndRemoveEmptyFields; + +@RunWith(DataProviderRunner.class) +public class MultivaluePropertyTest { + private static final String[] EMPTY_STRING_ARRAY = {}; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + @UseDataProvider("testParseAsCsv") + public void parseAsCsv_for_coverage(String value, String[] expected) { + // parseAsCsv is extensively tested in org.sonar.server.config.ConfigurationProviderTest + assertThat(parseAsCsv("key", value)) + .isEqualTo(parseAsCsv("key", value, Function.identity())) + .isEqualTo(expected); + } + + @Test + public void parseAsCsv_fails_with_ISE_if_value_can_not_be_parsed() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Property: 'multi' doesn't contain a valid CSV value: '\"a ,b'"); + + parseAsCsv("multi", "\"a ,b"); + } + + @DataProvider + public static Object[][] testParseAsCsv() { + return new Object[][] { + {"a", arrayOf("a")}, + {" a", arrayOf("a")}, + {"a ", arrayOf("a")}, + {" a, b", arrayOf("a", "b")}, + {"a,b ", arrayOf("a", "b")}, + {"a,,,b,c,,d", arrayOf("a", "b", "c", "d")}, + {" , \n ,, \t", EMPTY_STRING_ARRAY}, + {"\" a\"", arrayOf(" a")}, + {"\",\"", arrayOf(",")}, + // escaped quote in quoted field + {"\"\"\"\"", arrayOf("\"")} + }; + } + + private static String[] arrayOf(String... strs) { + return strs; + } + + @Test + public void trimFieldsAndRemoveEmptyFields_throws_NPE_if_arg_is_null() { + expectedException.expect(NullPointerException.class); + + trimFieldsAndRemoveEmptyFields(null); + } + + @Test + @UseDataProvider("plains") + public void trimFieldsAndRemoveEmptyFields_ignores_EmptyFields(String str) { + assertThat(trimFieldsAndRemoveEmptyFields("")).isEqualTo(""); + assertThat(trimFieldsAndRemoveEmptyFields(str)).isEqualTo(str); + + assertThat(trimFieldsAndRemoveEmptyFields(',' + str)).isEqualTo(str); + assertThat(trimFieldsAndRemoveEmptyFields(str + ',')).isEqualTo(str); + assertThat(trimFieldsAndRemoveEmptyFields(",,," + str)).isEqualTo(str); + assertThat(trimFieldsAndRemoveEmptyFields(str + ",,,")).isEqualTo(str); + + assertThat(trimFieldsAndRemoveEmptyFields(str + ',' + str)).isEqualTo(str + ',' + str); + assertThat(trimFieldsAndRemoveEmptyFields(str + ",,," + str)).isEqualTo(str + ',' + str); + assertThat(trimFieldsAndRemoveEmptyFields(',' + str + ',' + str)).isEqualTo(str + ',' + str); + assertThat(trimFieldsAndRemoveEmptyFields("," + str + ",,," + str)).isEqualTo(str + ',' + str); + assertThat(trimFieldsAndRemoveEmptyFields(",,," + str + ",,," + str)).isEqualTo(str + ',' + str); + + assertThat(trimFieldsAndRemoveEmptyFields(str + ',' + str + ',')).isEqualTo(str + ',' + str); + assertThat(trimFieldsAndRemoveEmptyFields(str + ",,," + str + ",")).isEqualTo(str + ',' + str); + assertThat(trimFieldsAndRemoveEmptyFields(str + ",,," + str + ",,")).isEqualTo(str + ',' + str); + + assertThat(trimFieldsAndRemoveEmptyFields(',' + str + ',' + str + ',')).isEqualTo(str + ',' + str); + assertThat(trimFieldsAndRemoveEmptyFields(",," + str + ',' + str + ',')).isEqualTo(str + ',' + str); + assertThat(trimFieldsAndRemoveEmptyFields(',' + str + ",," + str + ',')).isEqualTo(str + ',' + str); + assertThat(trimFieldsAndRemoveEmptyFields(',' + str + ',' + str + ",,")).isEqualTo(str + ',' + str); + assertThat(trimFieldsAndRemoveEmptyFields(",,," + str + ",,," + str + ",,")).isEqualTo(str + ',' + str); + + assertThat(trimFieldsAndRemoveEmptyFields(str + ',' + str + ',' + str)).isEqualTo(str + ',' + str + ',' + str); + assertThat(trimFieldsAndRemoveEmptyFields(str + ',' + str + ',' + str)).isEqualTo(str + ',' + str + ',' + str); + } + + @DataProvider + public static Object[][] plains() { + return new Object[][] { + {randomAlphanumeric(1)}, + {randomAlphanumeric(2)}, + {randomAlphanumeric(3 + new Random().nextInt(5))} + }; + } + + @Test + @UseDataProvider("emptyAndtrimmable") + public void trimFieldsAndRemoveEmptyFields_ignores_empty_fields_and_trims_fields(String empty, String trimmable) { + String expected = trimmable.trim(); + assertThat(empty.trim()).isEmpty(); + + assertThat(trimFieldsAndRemoveEmptyFields(trimmable)).isEqualTo(expected); + assertThat(trimFieldsAndRemoveEmptyFields(trimmable + ',' + empty)).isEqualTo(expected); + assertThat(trimFieldsAndRemoveEmptyFields(trimmable + ",," + empty)).isEqualTo(expected); + assertThat(trimFieldsAndRemoveEmptyFields(empty + ',' + trimmable)).isEqualTo(expected); + assertThat(trimFieldsAndRemoveEmptyFields(empty + ",," + trimmable)).isEqualTo(expected); + assertThat(trimFieldsAndRemoveEmptyFields(empty + ',' + trimmable + ',' + empty)).isEqualTo(expected); + assertThat(trimFieldsAndRemoveEmptyFields(empty + ",," + trimmable + ",,," + empty)).isEqualTo(expected); + + assertThat(trimFieldsAndRemoveEmptyFields(trimmable + ',' + empty + ',' + empty)).isEqualTo(expected); + assertThat(trimFieldsAndRemoveEmptyFields(trimmable + ",," + empty + ",,," + empty)).isEqualTo(expected); + + assertThat(trimFieldsAndRemoveEmptyFields(empty + ',' + empty + ',' + trimmable)).isEqualTo(expected); + assertThat(trimFieldsAndRemoveEmptyFields(empty + ",,,," + empty + ",," + trimmable)).isEqualTo(expected); + + assertThat(trimFieldsAndRemoveEmptyFields(trimmable + ',' + trimmable)).isEqualTo(expected + ',' + expected); + assertThat(trimFieldsAndRemoveEmptyFields(trimmable + ',' + trimmable + ',' + trimmable)).isEqualTo(expected + ',' + expected + ',' + expected); + assertThat(trimFieldsAndRemoveEmptyFields(trimmable + "," + trimmable + ',' + trimmable)).isEqualTo(expected + ',' + expected + ',' + expected); + } + + @Test + public void trimAccordingToStringTrim() { + String str = randomAlphanumeric(4); + for (int i = 0; i <= ' '; i++) { + String prefixed = (char) i + str; + String suffixed = (char) i + str; + String both = (char) i + str + (char) i; + assertThat(trimFieldsAndRemoveEmptyFields(prefixed)).isEqualTo(prefixed.trim()); + assertThat(trimFieldsAndRemoveEmptyFields(suffixed)).isEqualTo(suffixed.trim()); + assertThat(trimFieldsAndRemoveEmptyFields(both)).isEqualTo(both.trim()); + } + } + + @DataProvider + public static Object[][] emptyAndtrimmable() { + Random random = new Random(); + String oneEmpty = randomTrimmedChars(1, random); + String twoEmpty = randomTrimmedChars(2, random); + String threePlusEmpty = randomTrimmedChars(3 + random.nextInt(5), random); + String onePlusEmpty = randomTrimmedChars(1 + random.nextInt(5), random); + + String plain = randomAlphanumeric(1); + String plainWithtrimmable = randomAlphanumeric(2) + onePlusEmpty + randomAlphanumeric(3); + String quotedWithSeparator = '"' + randomAlphanumeric(3) + ',' + randomAlphanumeric(2) + '"'; + String quotedWithDoubleSeparator = '"' + randomAlphanumeric(3) + ",," + randomAlphanumeric(2) + '"'; + String quotedWithtrimmable = '"' + randomAlphanumeric(3) + onePlusEmpty + randomAlphanumeric(2) + '"'; + + String[] empties = {oneEmpty, twoEmpty, threePlusEmpty}; + String[] strings = {plain, plainWithtrimmable, + onePlusEmpty + plain, plain + onePlusEmpty, onePlusEmpty + plain + onePlusEmpty, + onePlusEmpty + plainWithtrimmable, plainWithtrimmable + onePlusEmpty, onePlusEmpty + plainWithtrimmable + onePlusEmpty, + onePlusEmpty + quotedWithSeparator, quotedWithSeparator + onePlusEmpty, onePlusEmpty + quotedWithSeparator + onePlusEmpty, + onePlusEmpty + quotedWithDoubleSeparator, quotedWithDoubleSeparator + onePlusEmpty, onePlusEmpty + quotedWithDoubleSeparator + onePlusEmpty, + onePlusEmpty + quotedWithtrimmable, quotedWithtrimmable + onePlusEmpty, onePlusEmpty + quotedWithtrimmable + onePlusEmpty + }; + + Object[][] res = new Object[empties.length * strings.length][2]; + int i = 0; + for (String empty : empties) { + for (String string : strings) { + res[i][0] = empty; + res[i][1] = string; + i++; + } + } + return res; + } + + @Test + @UseDataProvider("emptys") + public void trimFieldsAndRemoveEmptyFields_quotes_allow_to_preserve_fields(String empty) { + String quotedEmpty = '"' + empty + '"'; + + assertThat(trimFieldsAndRemoveEmptyFields(quotedEmpty)).isEqualTo(quotedEmpty); + assertThat(trimFieldsAndRemoveEmptyFields(',' + quotedEmpty)).isEqualTo(quotedEmpty); + assertThat(trimFieldsAndRemoveEmptyFields(quotedEmpty + ',')).isEqualTo(quotedEmpty); + assertThat(trimFieldsAndRemoveEmptyFields(',' + quotedEmpty + ',')).isEqualTo(quotedEmpty); + + assertThat(trimFieldsAndRemoveEmptyFields(quotedEmpty + ',' + quotedEmpty)).isEqualTo(quotedEmpty + ',' + quotedEmpty); + assertThat(trimFieldsAndRemoveEmptyFields(quotedEmpty + ",," + quotedEmpty)).isEqualTo(quotedEmpty + ',' + quotedEmpty); + + assertThat(trimFieldsAndRemoveEmptyFields(quotedEmpty + ',' + quotedEmpty + ',' + quotedEmpty)).isEqualTo(quotedEmpty + ',' + quotedEmpty + ',' + quotedEmpty); + } + + @DataProvider + public static Object[][] emptys() { + Random random = new Random(); + return new Object[][] { + {randomTrimmedChars(1, random)}, + {randomTrimmedChars(2, random)}, + {randomTrimmedChars(3 + random.nextInt(5), random)} + }; + } + + @Test + public void trimFieldsAndRemoveEmptyFields_supports_escaped_quote_in_quotes() { + assertThat(trimFieldsAndRemoveEmptyFields("\"f\"\"oo\"")).isEqualTo("\"f\"\"oo\""); + assertThat(trimFieldsAndRemoveEmptyFields("\"f\"\"oo\",\"bar\"\"\"")).isEqualTo("\"f\"\"oo\",\"bar\"\"\""); + } + + @Test + public void trimFieldsAndRemoveEmptyFields_does_not_fail_on_unbalanced_quotes() { + assertThat(trimFieldsAndRemoveEmptyFields("\"")).isEqualTo("\""); + assertThat(trimFieldsAndRemoveEmptyFields("\"foo")).isEqualTo("\"foo"); + assertThat(trimFieldsAndRemoveEmptyFields("foo\"")).isEqualTo("foo\""); + + assertThat(trimFieldsAndRemoveEmptyFields("\"foo\",\"")).isEqualTo("\"foo\",\""); + assertThat(trimFieldsAndRemoveEmptyFields("\",\"foo\"")).isEqualTo("\",\"foo\""); + + assertThat(trimFieldsAndRemoveEmptyFields("\"foo\",\", ")).isEqualTo("\"foo\",\", "); + + assertThat(trimFieldsAndRemoveEmptyFields(" a ,,b , c, \"foo\",\" ")).isEqualTo("a,b,c,\"foo\",\" "); + assertThat(trimFieldsAndRemoveEmptyFields("\" a ,,b , c, ")).isEqualTo("\" a ,,b , c, "); + } + + private static final char[] SOME_PRINTABLE_TRIMMABLE_CHARS = { + ' ', '\t', '\n', '\r' + }; + + /** + * Result of randomTrimmedChars being used as arguments to JUnit test method through the DataProvider feature, they + * are printed to surefire report. Some of those chars breaks the parsing of the surefire report during sonar analysis. + * Therefor, we only use a subset of the trimmable chars. + */ + private static String randomTrimmedChars(int length, Random random) { + char[] chars = new char[length]; + for (int i = 0; i < chars.length; i++) { + chars[i] = SOME_PRINTABLE_TRIMMABLE_CHARS[random.nextInt(SOME_PRINTABLE_TRIMMABLE_CHARS.length)]; + } + return new String(chars); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/context/MetadataLoaderTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/context/MetadataLoaderTest.java new file mode 100644 index 00000000000..e6106a3dfe3 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/context/MetadataLoaderTest.java @@ -0,0 +1,80 @@ +/* + * 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.impl.context; + +import java.io.File; +import java.net.MalformedURLException; +import org.sonar.api.SonarEdition; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.impl.context.MetadataLoader; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.Version; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MetadataLoaderTest { + private System2 system = mock(System2.class); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void load_version_from_file_in_classpath() { + Version version = org.sonar.api.impl.context.MetadataLoader.loadVersion(System2.INSTANCE); + assertThat(version).isNotNull(); + assertThat(version.major()).isGreaterThanOrEqualTo(5); + } + + @Test + public void load_edition_from_file_in_classpath() { + SonarEdition edition = org.sonar.api.impl.context.MetadataLoader.loadEdition(System2.INSTANCE); + assertThat(edition).isNotNull(); + } + + @Test + public void load_edition_defaults_to_community_if_file_not_found() throws MalformedURLException { + when(system.getResource(anyString())).thenReturn(new File("target/unknown").toURI().toURL()); + SonarEdition edition = org.sonar.api.impl.context.MetadataLoader.loadEdition(System2.INSTANCE); + assertThat(edition).isEqualTo(SonarEdition.COMMUNITY); + } + + @Test + public void throw_ISE_if_edition_is_invalid() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Invalid edition found in '/sonar-edition.txt': 'TRASH'"); + + org.sonar.api.impl.context.MetadataLoader.parseEdition("trash"); + } + + @Test + public void throw_ISE_if_fail_to_load_version() throws Exception { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Can not load /sonar-api-version.txt from classpath"); + + when(system.getResource(anyString())).thenReturn(new File("target/unknown").toURI().toURL()); + MetadataLoader.loadVersion(system); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/context/SonarRuntimeImplTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/context/SonarRuntimeImplTest.java new file mode 100644 index 00000000000..617dbafeda2 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/context/SonarRuntimeImplTest.java @@ -0,0 +1,68 @@ +/* + * 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.impl.context; + +import org.sonar.api.SonarEdition; +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.SonarProduct; +import org.sonar.api.SonarQubeSide; +import org.sonar.api.SonarRuntime; +import org.sonar.api.impl.context.SonarRuntimeImpl; +import org.sonar.api.utils.Version; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SonarRuntimeImplTest { + + private static final Version A_VERSION = Version.parse("6.0"); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void sonarQube_environment() { + SonarRuntime apiVersion = org.sonar.api.impl.context.SonarRuntimeImpl.forSonarQube(A_VERSION, SonarQubeSide.SCANNER, SonarEdition.COMMUNITY); + assertThat(apiVersion.getApiVersion()).isEqualTo(A_VERSION); + assertThat(apiVersion.getProduct()).isEqualTo(SonarProduct.SONARQUBE); + assertThat(apiVersion.getSonarQubeSide()).isEqualTo(SonarQubeSide.SCANNER); + } + + @Test + public void sonarLint_environment() { + SonarRuntime apiVersion = org.sonar.api.impl.context.SonarRuntimeImpl.forSonarLint(A_VERSION); + assertThat(apiVersion.getApiVersion()).isEqualTo(A_VERSION); + assertThat(apiVersion.getProduct()).isEqualTo(SonarProduct.SONARLINT); + try { + apiVersion.getSonarQubeSide(); + Assertions.fail("Expected exception"); + } catch (Exception e) { + assertThat(e).isInstanceOf(UnsupportedOperationException.class); + } + } + + @Test(expected = IllegalArgumentException.class) + public void sonarqube_requires_side() { + SonarRuntimeImpl.forSonarQube(A_VERSION, null, null); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/DefaultFileSystemTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/DefaultFileSystemTest.java new file mode 100644 index 00000000000..552387c70e6 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/DefaultFileSystemTest.java @@ -0,0 +1,140 @@ +/* + * 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.impl.fs; + +import java.io.File; +import java.nio.charset.Charset; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.impl.fs.TestInputFileBuilder; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DefaultFileSystemTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private DefaultFileSystem fs; + + private File basedir; + + @Before + public void prepare() throws Exception { + basedir = temp.newFolder(); + fs = new DefaultFileSystem(basedir.toPath()); + } + + @Test + public void test_directories() throws Exception { + assertThat(fs.baseDir()).isAbsolute().isDirectory().exists(); + assertThat(fs.baseDir().getCanonicalPath()).isEqualTo(basedir.getCanonicalPath()); + + File workdir = temp.newFolder(); + fs.setWorkDir(workdir.toPath()); + assertThat(fs.workDir()).isAbsolute().isDirectory().exists(); + assertThat(fs.workDir().getCanonicalPath()).isEqualTo(workdir.getCanonicalPath()); + } + + @Test + public void test_encoding() throws Exception { + fs.setEncoding(Charset.forName("ISO-8859-1")); + assertThat(fs.encoding()).isEqualTo(Charset.forName("ISO-8859-1")); + } + + @Test + public void add_languages() { + assertThat(fs.languages()).isEmpty(); + + fs.add(new TestInputFileBuilder("foo", "src/Foo.php").setLanguage("php").build()); + fs.add(new TestInputFileBuilder("foo", "src/Bar.java").setLanguage("java").build()); + + assertThat(fs.languages()).containsOnly("java", "php"); + } + + @Test + public void files() { + assertThat(fs.inputFiles(fs.predicates().all())).isEmpty(); + + fs.add(new TestInputFileBuilder("foo", "src/Foo.php").setLanguage("php").build()); + fs.add(new TestInputFileBuilder("foo", "src/Bar.java").setLanguage("java").build()); + fs.add(new TestInputFileBuilder("foo", "src/Baz.java").setLanguage("java").build()); + + // no language + fs.add(new TestInputFileBuilder("foo", "src/readme.txt").build()); + + assertThat(fs.inputFile(fs.predicates().hasRelativePath("src/Bar.java"))).isNotNull(); + assertThat(fs.inputFile(fs.predicates().hasRelativePath("does/not/exist"))).isNull(); + + assertThat(fs.inputFile(fs.predicates().hasAbsolutePath(new File(basedir, "src/Bar.java").getAbsolutePath()))).isNotNull(); + assertThat(fs.inputFile(fs.predicates().hasAbsolutePath(new File(basedir, "does/not/exist").getAbsolutePath()))).isNull(); + assertThat(fs.inputFile(fs.predicates().hasAbsolutePath(new File(basedir, "../src/Bar.java").getAbsolutePath()))).isNull(); + + assertThat(fs.inputFile(fs.predicates().hasURI(new File(basedir, "src/Bar.java").toURI()))).isNotNull(); + assertThat(fs.inputFile(fs.predicates().hasURI(new File(basedir, "does/not/exist").toURI()))).isNull(); + assertThat(fs.inputFile(fs.predicates().hasURI(new File(basedir, "../src/Bar.java").toURI()))).isNull(); + + assertThat(fs.files(fs.predicates().all())).hasSize(4); + assertThat(fs.files(fs.predicates().hasLanguage("java"))).hasSize(2); + assertThat(fs.files(fs.predicates().hasLanguage("cobol"))).isEmpty(); + + assertThat(fs.hasFiles(fs.predicates().all())).isTrue(); + assertThat(fs.hasFiles(fs.predicates().hasLanguage("java"))).isTrue(); + assertThat(fs.hasFiles(fs.predicates().hasLanguage("cobol"))).isFalse(); + + assertThat(fs.inputFiles(fs.predicates().all())).hasSize(4); + assertThat(fs.inputFiles(fs.predicates().hasLanguage("php"))).hasSize(1); + assertThat(fs.inputFiles(fs.predicates().hasLanguage("java"))).hasSize(2); + assertThat(fs.inputFiles(fs.predicates().hasLanguage("cobol"))).isEmpty(); + + assertThat(fs.languages()).containsOnly("java", "php"); + } + + @Test + public void input_file_returns_null_if_file_not_found() { + assertThat(fs.inputFile(fs.predicates().hasRelativePath("src/Bar.java"))).isNull(); + assertThat(fs.inputFile(fs.predicates().hasLanguage("cobol"))).isNull(); + } + + @Test + public void input_file_fails_if_too_many_results() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("expected one element"); + + fs.add(new TestInputFileBuilder("foo", "src/Bar.java").setLanguage("java").build()); + fs.add(new TestInputFileBuilder("foo", "src/Baz.java").setLanguage("java").build()); + + fs.inputFile(fs.predicates().all()); + } + + @Test + public void input_file_supports_non_indexed_predicates() { + fs.add(new TestInputFileBuilder("foo", "src/Bar.java").setLanguage("java").build()); + + // it would fail if more than one java file + assertThat(fs.inputFile(fs.predicates().hasLanguage("java"))).isNotNull(); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/DefaultInputDirTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/DefaultInputDirTest.java new file mode 100644 index 00000000000..263032b33f2 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/DefaultInputDirTest.java @@ -0,0 +1,64 @@ +/* + * 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.impl.fs; + +import java.io.File; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DefaultInputDirTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void test() throws Exception { + File baseDir = temp.newFolder(); + DefaultInputDir inputDir = new DefaultInputDir("ABCDE", "src") + .setModuleBaseDir(baseDir.toPath()); + + assertThat(inputDir.key()).isEqualTo("ABCDE:src"); + assertThat(inputDir.file().getAbsolutePath()).isEqualTo(new File(baseDir, "src").getAbsolutePath()); + assertThat(inputDir.relativePath()).isEqualTo("src"); + assertThat(new File(inputDir.relativePath())).isRelative(); + assertThat(inputDir.absolutePath()).endsWith("src"); + assertThat(new File(inputDir.absolutePath())).isAbsolute(); + } + + @Test + public void testEqualsAndHashCode() throws Exception { + DefaultInputDir inputDir1 = new DefaultInputDir("ABCDE", "src"); + + DefaultInputDir inputDir2 = new DefaultInputDir("ABCDE", "src"); + + assertThat(inputDir1.equals(inputDir1)).isTrue(); + assertThat(inputDir1.equals(inputDir2)).isTrue(); + assertThat(inputDir1.equals("foo")).isFalse(); + + assertThat(inputDir1.hashCode()).isEqualTo(63545559); + + assertThat(inputDir1.toString()).contains("[moduleKey=ABCDE, relative=src, basedir=null"); + + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/DefaultInputFileTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/DefaultInputFileTest.java new file mode 100644 index 00000000000..7e22203de37 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/DefaultInputFileTest.java @@ -0,0 +1,313 @@ +/* + * 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.impl.fs; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.stream.Collectors; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.TextRange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +public class DefaultInputFileTest { + + private static final String PROJECT_RELATIVE_PATH = "module1/src/Foo.php"; + private static final String MODULE_RELATIVE_PATH = "src/Foo.php"; + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private DefaultIndexedFile indexedFile; + + private Path baseDir; + private SensorStrategy sensorStrategy; + + @Before + public void prepare() throws IOException { + baseDir = temp.newFolder().toPath(); + sensorStrategy = new SensorStrategy(); + indexedFile = new DefaultIndexedFile(baseDir.resolve(PROJECT_RELATIVE_PATH), "ABCDE", PROJECT_RELATIVE_PATH, MODULE_RELATIVE_PATH, InputFile.Type.TEST, "php", 0, + sensorStrategy); + } + + @Test + public void test() throws Exception { + + Metadata metadata = new Metadata(42, 42, "", new int[0], new int[0], 10); + DefaultInputFile inputFile = new DefaultInputFile(indexedFile, (f) -> f.setMetadata(metadata)) + .setStatus(InputFile.Status.ADDED) + .setCharset(StandardCharsets.ISO_8859_1); + + assertThat(inputFile.absolutePath()).endsWith("Foo.php"); + assertThat(inputFile.filename()).isEqualTo("Foo.php"); + assertThat(inputFile.uri()).hasPath(baseDir.resolve(PROJECT_RELATIVE_PATH).toUri().getPath()); + assertThat(new File(inputFile.absolutePath())).isAbsolute(); + assertThat(inputFile.language()).isEqualTo("php"); + assertThat(inputFile.status()).isEqualTo(InputFile.Status.ADDED); + assertThat(inputFile.type()).isEqualTo(InputFile.Type.TEST); + assertThat(inputFile.lines()).isEqualTo(42); + assertThat(inputFile.charset()).isEqualTo(StandardCharsets.ISO_8859_1); + + assertThat(inputFile.getModuleRelativePath()).isEqualTo(MODULE_RELATIVE_PATH); + assertThat(inputFile.getProjectRelativePath()).isEqualTo(PROJECT_RELATIVE_PATH); + + sensorStrategy.setGlobal(false); + assertThat(inputFile.relativePath()).isEqualTo(MODULE_RELATIVE_PATH); + assertThat(new File(inputFile.relativePath())).isRelative(); + sensorStrategy.setGlobal(true); + assertThat(inputFile.relativePath()).isEqualTo(PROJECT_RELATIVE_PATH); + assertThat(new File(inputFile.relativePath())).isRelative(); + } + + @Test + public void test_content() throws IOException { + Path testFile = baseDir.resolve(PROJECT_RELATIVE_PATH); + Files.createDirectories(testFile.getParent()); + String content = "test é string"; + Files.write(testFile, content.getBytes(StandardCharsets.ISO_8859_1)); + + assertThat(Files.readAllLines(testFile, StandardCharsets.ISO_8859_1).get(0)).hasSize(content.length()); + + Metadata metadata = new Metadata(42, 30, "", new int[0], new int[0], 10); + + DefaultInputFile inputFile = new DefaultInputFile(indexedFile, f -> f.setMetadata(metadata)) + .setStatus(InputFile.Status.ADDED) + .setCharset(StandardCharsets.ISO_8859_1); + + assertThat(inputFile.contents()).isEqualTo(content); + try (InputStream inputStream = inputFile.inputStream()) { + String result = new BufferedReader(new InputStreamReader(inputStream, inputFile.charset())).lines().collect(Collectors.joining()); + assertThat(result).isEqualTo(content); + } + + } + + @Test + public void test_content_exclude_bom() throws IOException { + Path testFile = baseDir.resolve(PROJECT_RELATIVE_PATH); + Files.createDirectories(testFile.getParent()); + try (BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(testFile.toFile()), StandardCharsets.UTF_8))) { + out.write('\ufeff'); + } + String content = "test é string €"; + Files.write(testFile, content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND); + + assertThat(Files.readAllLines(testFile, StandardCharsets.UTF_8).get(0)).hasSize(content.length() + 1); + + Metadata metadata = new Metadata(42, 30, "", new int[0], new int[0], 10); + + DefaultInputFile inputFile = new DefaultInputFile(indexedFile, f -> f.setMetadata(metadata)) + .setStatus(InputFile.Status.ADDED) + .setCharset(StandardCharsets.UTF_8); + + assertThat(inputFile.contents()).isEqualTo(content); + try (InputStream inputStream = inputFile.inputStream()) { + String result = new BufferedReader(new InputStreamReader(inputStream, inputFile.charset())).lines().collect(Collectors.joining()); + assertThat(result).isEqualTo(content); + } + + } + + @Test + public void test_equals_and_hashcode() throws Exception { + DefaultInputFile f1 = new DefaultInputFile(new DefaultIndexedFile("ABCDE", Paths.get("module"), MODULE_RELATIVE_PATH, null), (f) -> mock(Metadata.class)); + DefaultInputFile f1a = new DefaultInputFile(new DefaultIndexedFile("ABCDE", Paths.get("module"), MODULE_RELATIVE_PATH, null), (f) -> mock(Metadata.class)); + DefaultInputFile f2 = new DefaultInputFile(new DefaultIndexedFile("ABCDE", Paths.get("module"), "src/Bar.php", null), (f) -> mock(Metadata.class)); + + assertThat(f1).isEqualTo(f1); + assertThat(f1).isEqualTo(f1a); + assertThat(f1).isNotEqualTo(f2); + assertThat(f1.equals("foo")).isFalse(); + assertThat(f1.equals(null)).isFalse(); + + assertThat(f1.hashCode()).isEqualTo(f1.hashCode()); + assertThat(f1.hashCode()).isEqualTo(f1a.hashCode()); + } + + @Test + public void test_toString() throws Exception { + DefaultInputFile file = new DefaultInputFile(new DefaultIndexedFile("ABCDE", Paths.get("module"), MODULE_RELATIVE_PATH, null), (f) -> mock(Metadata.class)); + assertThat(file.toString()).isEqualTo(MODULE_RELATIVE_PATH); + } + + @Test + public void checkValidPointer() { + Metadata metadata = new Metadata(2, 2, "", new int[] {0, 10}, new int[] {9, 15}, 16); + DefaultInputFile file = new DefaultInputFile(new DefaultIndexedFile("ABCDE", Paths.get("module"), MODULE_RELATIVE_PATH, null), f -> f.setMetadata(metadata)); + assertThat(file.newPointer(1, 0).line()).isEqualTo(1); + assertThat(file.newPointer(1, 0).lineOffset()).isEqualTo(0); + // Don't fail + file.newPointer(1, 9); + file.newPointer(2, 0); + file.newPointer(2, 5); + + try { + file.newPointer(0, 1); + fail(); + } catch (Exception e) { + assertThat(e).hasMessage("0 is not a valid line for a file"); + } + try { + file.newPointer(3, 1); + fail(); + } catch (Exception e) { + assertThat(e).hasMessage("3 is not a valid line for pointer. File src/Foo.php has 2 line(s)"); + } + try { + file.newPointer(1, -1); + fail(); + } catch (Exception e) { + assertThat(e).hasMessage("-1 is not a valid line offset for a file"); + } + try { + file.newPointer(1, 10); + fail(); + } catch (Exception e) { + assertThat(e).hasMessage("10 is not a valid line offset for pointer. File src/Foo.php has 9 character(s) at line 1"); + } + } + + @Test + public void checkValidPointerUsingGlobalOffset() { + Metadata metadata = new Metadata(2, 2, "", new int[] {0, 10}, new int[] {8, 15}, 16); + DefaultInputFile file = new DefaultInputFile(new DefaultIndexedFile("ABCDE", Paths.get("module"), MODULE_RELATIVE_PATH, null), f -> f.setMetadata(metadata)); + assertThat(file.newPointer(0).line()).isEqualTo(1); + assertThat(file.newPointer(0).lineOffset()).isEqualTo(0); + + assertThat(file.newPointer(9).line()).isEqualTo(1); + // Ignore eol characters + assertThat(file.newPointer(9).lineOffset()).isEqualTo(8); + + assertThat(file.newPointer(10).line()).isEqualTo(2); + assertThat(file.newPointer(10).lineOffset()).isEqualTo(0); + + assertThat(file.newPointer(15).line()).isEqualTo(2); + assertThat(file.newPointer(15).lineOffset()).isEqualTo(5); + + assertThat(file.newPointer(16).line()).isEqualTo(2); + // Ignore eol characters + assertThat(file.newPointer(16).lineOffset()).isEqualTo(5); + + try { + file.newPointer(-1); + fail(); + } catch (Exception e) { + assertThat(e).hasMessage("-1 is not a valid offset for a file"); + } + + try { + file.newPointer(17); + fail(); + } catch (Exception e) { + assertThat(e).hasMessage("17 is not a valid offset for file src/Foo.php. Max offset is 16"); + } + } + + @Test + public void checkValidRange() { + Metadata metadata = new FileMetadata().readMetadata(new StringReader("bla bla a\nabcde")); + DefaultInputFile file = new DefaultInputFile(new DefaultIndexedFile("ABCDE", Paths.get("module"), MODULE_RELATIVE_PATH, null), f -> f.setMetadata(metadata)); + + assertThat(file.newRange(file.newPointer(1, 0), file.newPointer(2, 1)).start().line()).isEqualTo(1); + // Don't fail + file.newRange(file.newPointer(1, 0), file.newPointer(1, 1)); + file.newRange(file.newPointer(1, 0), file.newPointer(1, 9)); + file.newRange(file.newPointer(1, 0), file.newPointer(2, 0)); + assertThat(file.newRange(file.newPointer(1, 0), file.newPointer(2, 5))).isEqualTo(file.newRange(0, 15)); + + try { + file.newRange(file.newPointer(1, 0), file.newPointer(1, 0)); + fail(); + } catch (Exception e) { + assertThat(e).hasMessage("Start pointer [line=1, lineOffset=0] should be before end pointer [line=1, lineOffset=0]"); + } + try { + file.newRange(file.newPointer(1, 0), file.newPointer(1, 10)); + fail(); + } catch (Exception e) { + assertThat(e).hasMessage("10 is not a valid line offset for pointer. File src/Foo.php has 9 character(s) at line 1"); + } + } + + @Test + public void selectLine() { + Metadata metadata = new FileMetadata().readMetadata(new StringReader("bla bla a\nabcde\n\nabc")); + DefaultInputFile file = new DefaultInputFile(new DefaultIndexedFile("ABCDE", Paths.get("module"), MODULE_RELATIVE_PATH, null), f -> f.setMetadata(metadata)); + + assertThat(file.selectLine(1).start().line()).isEqualTo(1); + assertThat(file.selectLine(1).start().lineOffset()).isEqualTo(0); + assertThat(file.selectLine(1).end().line()).isEqualTo(1); + assertThat(file.selectLine(1).end().lineOffset()).isEqualTo(9); + + // Don't fail when selecting empty line + assertThat(file.selectLine(3).start().line()).isEqualTo(3); + assertThat(file.selectLine(3).start().lineOffset()).isEqualTo(0); + assertThat(file.selectLine(3).end().line()).isEqualTo(3); + assertThat(file.selectLine(3).end().lineOffset()).isEqualTo(0); + + try { + file.selectLine(5); + fail(); + } catch (Exception e) { + assertThat(e).hasMessage("5 is not a valid line for pointer. File src/Foo.php has 4 line(s)"); + } + } + + @Test + public void checkValidRangeUsingGlobalOffset() { + Metadata metadata = new Metadata(2, 2, "", new int[] {0, 10}, new int[] {9, 15}, 16); + DefaultInputFile file = new DefaultInputFile(new DefaultIndexedFile("ABCDE", Paths.get("module"), MODULE_RELATIVE_PATH, null), f -> f.setMetadata(metadata)); + TextRange newRange = file.newRange(10, 13); + assertThat(newRange.start().line()).isEqualTo(2); + assertThat(newRange.start().lineOffset()).isEqualTo(0); + assertThat(newRange.end().line()).isEqualTo(2); + assertThat(newRange.end().lineOffset()).isEqualTo(3); + } + + @Test + public void testRangeOverlap() { + Metadata metadata = new Metadata(2, 2, "", new int[] {0, 10}, new int[] {9, 15}, 16); + DefaultInputFile file = new DefaultInputFile(new DefaultIndexedFile("ABCDE", Paths.get("module"), MODULE_RELATIVE_PATH, null), f -> f.setMetadata(metadata)); + // Don't fail + assertThat(file.newRange(file.newPointer(1, 0), file.newPointer(1, 1)).overlap(file.newRange(file.newPointer(1, 0), file.newPointer(1, 1)))).isTrue(); + assertThat(file.newRange(file.newPointer(1, 0), file.newPointer(1, 1)).overlap(file.newRange(file.newPointer(1, 0), file.newPointer(1, 2)))).isTrue(); + assertThat(file.newRange(file.newPointer(1, 0), file.newPointer(1, 1)).overlap(file.newRange(file.newPointer(1, 1), file.newPointer(1, 2)))).isFalse(); + assertThat(file.newRange(file.newPointer(1, 2), file.newPointer(1, 3)).overlap(file.newRange(file.newPointer(1, 0), file.newPointer(1, 2)))).isFalse(); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/DefaultInputModuleTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/DefaultInputModuleTest.java new file mode 100644 index 00000000000..ac1a2c40417 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/DefaultInputModuleTest.java @@ -0,0 +1,110 @@ +/* + * 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.impl.fs; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.bootstrap.ProjectDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DefaultInputModuleTest { + + private static final String FILE_1 = "file1"; + private static final String TEST_1 = "test1"; + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void check_getters() throws IOException { + ProjectDefinition def = ProjectDefinition.create(); + def.setKey("moduleKey"); + File baseDir = temp.newFolder(); + Path src = baseDir.toPath().resolve(FILE_1); + Files.createFile(src); + Path test = baseDir.toPath().resolve(TEST_1); + Files.createFile(test); + def.setBaseDir(baseDir); + File workDir = temp.newFolder(); + def.setWorkDir(workDir); + def.setSources(FILE_1); + def.setTests(TEST_1); + DefaultInputModule module = new DefaultInputModule(def); + + assertThat(module.key()).isEqualTo("moduleKey"); + assertThat(module.definition()).isEqualTo(def); + assertThat(module.getBranch()).isNull(); + assertThat(module.getBaseDir()).isEqualTo(baseDir.toPath()); + assertThat(module.getKeyWithBranch()).isEqualTo("moduleKey"); + assertThat(module.getWorkDir()).isEqualTo(workDir.toPath()); + assertThat(module.getEncoding()).isEqualTo(Charset.defaultCharset()); + assertThat(module.getSourceDirsOrFiles().get()).containsExactlyInAnyOrder(src); + assertThat(module.getTestDirsOrFiles().get()).containsExactlyInAnyOrder(test); + assertThat(module.getEncoding()).isEqualTo(Charset.defaultCharset()); + + assertThat(module.isFile()).isFalse(); + } + + @Test + public void no_sources() throws IOException { + ProjectDefinition def = ProjectDefinition.create(); + def.setKey("moduleKey"); + File baseDir = temp.newFolder(); + Path src = baseDir.toPath().resolve(FILE_1); + Files.createFile(src); + Path test = baseDir.toPath().resolve(TEST_1); + Files.createFile(test); + def.setBaseDir(baseDir); + File workDir = temp.newFolder(); + def.setWorkDir(workDir); + DefaultInputModule module = new DefaultInputModule(def); + + assertThat(module.key()).isEqualTo("moduleKey"); + assertThat(module.definition()).isEqualTo(def); + assertThat(module.getBranch()).isNull(); + assertThat(module.getBaseDir()).isEqualTo(baseDir.toPath()); + assertThat(module.getKeyWithBranch()).isEqualTo("moduleKey"); + assertThat(module.getWorkDir()).isEqualTo(workDir.toPath()); + assertThat(module.getEncoding()).isEqualTo(Charset.defaultCharset()); + assertThat(module.getSourceDirsOrFiles()).isNotPresent(); + assertThat(module.getTestDirsOrFiles()).isNotPresent(); + assertThat(module.getEncoding()).isEqualTo(Charset.defaultCharset()); + + assertThat(module.isFile()).isFalse(); + } + + @Test + public void working_directory_should_be_hidden() throws IOException { + ProjectDefinition def = ProjectDefinition.create(); + File workDir = temp.newFolder(".sonar"); + def.setWorkDir(workDir); + File baseDir = temp.newFolder(); + def.setBaseDir(baseDir); + DefaultInputModule module = new DefaultInputModule(def); + assertThat(workDir.isHidden()).isTrue(); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/DefaultInputProjectTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/DefaultInputProjectTest.java new file mode 100644 index 00000000000..bd3f4939af0 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/DefaultInputProjectTest.java @@ -0,0 +1,86 @@ +/* + * 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.impl.fs; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.bootstrap.ProjectDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DefaultInputProjectTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void testGetters() throws IOException { + ProjectDefinition def = ProjectDefinition.create(); + def.setKey("projectKey"); + def.setName("projectName"); + File baseDir = temp.newFolder(); + def.setBaseDir(baseDir); + def.setDescription("desc"); + File workDir = temp.newFolder(); + def.setWorkDir(workDir); + def.setSources("file1"); + def.setTests("test1"); + AbstractProjectOrModule project = new DefaultInputProject(def); + + assertThat(project.key()).isEqualTo("projectKey"); + assertThat(project.getName()).isEqualTo("projectName"); + assertThat(project.getOriginalName()).isEqualTo("projectName"); + assertThat(project.definition()).isEqualTo(def); + assertThat(project.getBranch()).isNull(); + assertThat(project.getBaseDir()).isEqualTo(baseDir.toPath()); + assertThat(project.getKeyWithBranch()).isEqualTo("projectKey"); + assertThat(project.getDescription()).isEqualTo("desc"); + assertThat(project.getWorkDir()).isEqualTo(workDir.toPath()); + assertThat(project.getEncoding()).isEqualTo(Charset.defaultCharset()); + + assertThat(project.properties()).hasSize(5); + + assertThat(project.isFile()).isFalse(); + } + + @Test + public void testEncoding() throws IOException { + ProjectDefinition def = ProjectDefinition.create(); + def.setKey("projectKey"); + def.setName("projectName"); + File baseDir = temp.newFolder(); + def.setBaseDir(baseDir); + def.setProjectVersion("version"); + def.setDescription("desc"); + File workDir = temp.newFolder(); + def.setWorkDir(workDir); + def.setSources("file1"); + def.setProperty("sonar.sourceEncoding", "UTF-16"); + AbstractProjectOrModule project = new DefaultInputProject(def); + + assertThat(project.getEncoding()).isEqualTo(StandardCharsets.UTF_16); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/FileMetadataTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/FileMetadataTest.java new file mode 100644 index 00000000000..0f3f1d5965e --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/FileMetadataTest.java @@ -0,0 +1,307 @@ +/* + * 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.impl.fs; + +import java.io.File; +import java.io.FileInputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import javax.annotation.Nullable; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; + +import static org.apache.commons.codec.digest.DigestUtils.md5Hex; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +public class FileMetadataTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public LogTester logTester = new LogTester(); + + @Test + public void empty_file() throws Exception { + File tempFile = temp.newFile(); + FileUtils.touch(tempFile); + + Metadata metadata = new FileMetadata().readMetadata(new FileInputStream(tempFile), StandardCharsets.UTF_8, tempFile.getName()); + assertThat(metadata.lines()).isEqualTo(1); + assertThat(metadata.nonBlankLines()).isEqualTo(0); + assertThat(metadata.hash()).isNotEmpty(); + assertThat(metadata.originalLineStartOffsets()).containsOnly(0); + assertThat(metadata.originalLineEndOffsets()).containsOnly(0); + assertThat(metadata.isEmpty()).isTrue(); + } + + @Test + public void windows_without_latest_eol() throws Exception { + File tempFile = temp.newFile(); + FileUtils.write(tempFile, "foo\r\nbar\r\nbaz", StandardCharsets.UTF_8, true); + + Metadata metadata = new FileMetadata().readMetadata(new FileInputStream(tempFile), StandardCharsets.UTF_8, tempFile.getName()); + assertThat(metadata.lines()).isEqualTo(3); + assertThat(metadata.nonBlankLines()).isEqualTo(3); + assertThat(metadata.hash()).isEqualTo(md5Hex("foo\nbar\nbaz")); + assertThat(metadata.originalLineStartOffsets()).containsOnly(0, 5, 10); + assertThat(metadata.originalLineEndOffsets()).containsOnly(3, 8, 13); + assertThat(metadata.isEmpty()).isFalse(); + } + + @Test + public void read_with_wrong_encoding() throws Exception { + File tempFile = temp.newFile(); + FileUtils.write(tempFile, "marker´s\n", Charset.forName("cp1252")); + + Metadata metadata = new FileMetadata().readMetadata(new FileInputStream(tempFile), StandardCharsets.UTF_8, tempFile.getName()); + assertThat(metadata.lines()).isEqualTo(2); + assertThat(metadata.hash()).isEqualTo(md5Hex("marker\ufffds\n")); + assertThat(metadata.originalLineStartOffsets()).containsOnly(0, 9); + } + + @Test + public void non_ascii_utf_8() throws Exception { + File tempFile = temp.newFile(); + FileUtils.write(tempFile, "föo\r\nbà r\r\n\u1D11Ebaßz\r\n", StandardCharsets.UTF_8, true); + + Metadata metadata = new FileMetadata().readMetadata(new FileInputStream(tempFile), StandardCharsets.UTF_8, tempFile.getName()); + assertThat(metadata.lines()).isEqualTo(4); + assertThat(metadata.nonBlankLines()).isEqualTo(3); + assertThat(metadata.hash()).isEqualTo(md5Hex("föo\nbà r\n\u1D11Ebaßz\n")); + assertThat(metadata.originalLineStartOffsets()).containsOnly(0, 5, 10, 18); + } + + @Test + public void non_ascii_utf_16() throws Exception { + File tempFile = temp.newFile(); + FileUtils.write(tempFile, "föo\r\nbà r\r\n\u1D11Ebaßz\r\n", StandardCharsets.UTF_16, true); + Metadata metadata = new FileMetadata().readMetadata(new FileInputStream(tempFile), StandardCharsets.UTF_16, tempFile.getName()); + assertThat(metadata.lines()).isEqualTo(4); + assertThat(metadata.nonBlankLines()).isEqualTo(3); + assertThat(metadata.hash()).isEqualTo(md5Hex("föo\nbà r\n\u1D11Ebaßz\n".getBytes(StandardCharsets.UTF_8))); + assertThat(metadata.originalLineStartOffsets()).containsOnly(0, 5, 10, 18); + } + + @Test + public void unix_without_latest_eol() throws Exception { + File tempFile = temp.newFile(); + FileUtils.write(tempFile, "foo\nbar\nbaz", StandardCharsets.UTF_8, true); + + Metadata metadata = new FileMetadata().readMetadata(new FileInputStream(tempFile), StandardCharsets.UTF_8, tempFile.getName()); + assertThat(metadata.lines()).isEqualTo(3); + assertThat(metadata.nonBlankLines()).isEqualTo(3); + assertThat(metadata.hash()).isEqualTo(md5Hex("foo\nbar\nbaz")); + assertThat(metadata.originalLineStartOffsets()).containsOnly(0, 4, 8); + assertThat(metadata.originalLineEndOffsets()).containsOnly(3, 7, 11); + assertThat(metadata.isEmpty()).isFalse(); + } + + @Test + public void unix_with_latest_eol() throws Exception { + File tempFile = temp.newFile(); + FileUtils.write(tempFile, "foo\nbar\nbaz\n", StandardCharsets.UTF_8, true); + + Metadata metadata = new FileMetadata().readMetadata(new FileInputStream(tempFile), StandardCharsets.UTF_8, tempFile.getName()); + assertThat(metadata.lines()).isEqualTo(4); + assertThat(metadata.nonBlankLines()).isEqualTo(3); + assertThat(metadata.hash()).isEqualTo(md5Hex("foo\nbar\nbaz\n")); + assertThat(metadata.originalLineStartOffsets()).containsOnly(0, 4, 8, 12); + assertThat(metadata.originalLineEndOffsets()).containsOnly(3, 7, 11, 12); + } + + @Test + public void mac_without_latest_eol() throws Exception { + File tempFile = temp.newFile(); + FileUtils.write(tempFile, "foo\rbar\rbaz", StandardCharsets.UTF_8, true); + + Metadata metadata = new FileMetadata().readMetadata(new FileInputStream(tempFile), StandardCharsets.UTF_8, tempFile.getName()); + assertThat(metadata.lines()).isEqualTo(3); + assertThat(metadata.nonBlankLines()).isEqualTo(3); + assertThat(metadata.hash()).isEqualTo(md5Hex("foo\nbar\nbaz")); + assertThat(metadata.originalLineStartOffsets()).containsOnly(0, 4, 8); + assertThat(metadata.originalLineEndOffsets()).containsOnly(3, 7, 11); + } + + @Test + public void mac_with_latest_eol() throws Exception { + File tempFile = temp.newFile(); + FileUtils.write(tempFile, "foo\rbar\rbaz\r", StandardCharsets.UTF_8, true); + + Metadata metadata = new FileMetadata().readMetadata(new FileInputStream(tempFile), StandardCharsets.UTF_8, tempFile.getName()); + assertThat(metadata.lines()).isEqualTo(4); + assertThat(metadata.nonBlankLines()).isEqualTo(3); + assertThat(metadata.hash()).isEqualTo(md5Hex("foo\nbar\nbaz\n")); + assertThat(metadata.originalLineStartOffsets()).containsOnly(0, 4, 8, 12); + assertThat(metadata.originalLineEndOffsets()).containsOnly(3, 7, 11, 12); + } + + @Test + public void mix_of_newlines_with_latest_eol() throws Exception { + File tempFile = temp.newFile(); + FileUtils.write(tempFile, "foo\nbar\r\nbaz\n", StandardCharsets.UTF_8, true); + + Metadata metadata = new FileMetadata().readMetadata(new FileInputStream(tempFile), StandardCharsets.UTF_8, tempFile.getName()); + assertThat(metadata.lines()).isEqualTo(4); + assertThat(metadata.nonBlankLines()).isEqualTo(3); + assertThat(metadata.hash()).isEqualTo(md5Hex("foo\nbar\nbaz\n")); + assertThat(metadata.originalLineStartOffsets()).containsOnly(0, 4, 9, 13); + assertThat(metadata.originalLineEndOffsets()).containsOnly(3, 7, 12, 13); + } + + @Test + public void several_new_lines() throws Exception { + File tempFile = temp.newFile(); + FileUtils.write(tempFile, "foo\n\n\nbar", StandardCharsets.UTF_8, true); + + Metadata metadata = new FileMetadata().readMetadata(new FileInputStream(tempFile), StandardCharsets.UTF_8, tempFile.getName()); + assertThat(metadata.lines()).isEqualTo(4); + assertThat(metadata.nonBlankLines()).isEqualTo(2); + assertThat(metadata.hash()).isEqualTo(md5Hex("foo\n\n\nbar")); + assertThat(metadata.originalLineStartOffsets()).containsOnly(0, 4, 5, 6); + assertThat(metadata.originalLineEndOffsets()).containsOnly(3, 4, 5, 9); + } + + @Test + public void mix_of_newlines_without_latest_eol() throws Exception { + File tempFile = temp.newFile(); + FileUtils.write(tempFile, "foo\nbar\r\nbaz", StandardCharsets.UTF_8, true); + + Metadata metadata = new FileMetadata().readMetadata(new FileInputStream(tempFile), StandardCharsets.UTF_8, tempFile.getName()); + assertThat(metadata.lines()).isEqualTo(3); + assertThat(metadata.nonBlankLines()).isEqualTo(3); + assertThat(metadata.hash()).isEqualTo(md5Hex("foo\nbar\nbaz")); + assertThat(metadata.originalLineStartOffsets()).containsOnly(0, 4, 9); + assertThat(metadata.originalLineEndOffsets()).containsOnly(3, 7, 12); + } + + @Test + public void start_with_newline() throws Exception { + File tempFile = temp.newFile(); + FileUtils.write(tempFile, "\nfoo\nbar\r\nbaz", StandardCharsets.UTF_8, true); + + Metadata metadata = new FileMetadata().readMetadata(new FileInputStream(tempFile), StandardCharsets.UTF_8, tempFile.getName()); + assertThat(metadata.lines()).isEqualTo(4); + assertThat(metadata.nonBlankLines()).isEqualTo(3); + assertThat(metadata.hash()).isEqualTo(md5Hex("\nfoo\nbar\nbaz")); + assertThat(metadata.originalLineStartOffsets()).containsOnly(0, 1, 5, 10); + assertThat(metadata.originalLineEndOffsets()).containsOnly(0, 4, 8, 13); + } + + @Test + public void ignore_whitespace_when_computing_line_hashes() throws Exception { + File tempFile = temp.newFile(); + FileUtils.write(tempFile, " foo\nb ar\r\nbaz \t", StandardCharsets.UTF_8, true); + + DefaultInputFile f = new TestInputFileBuilder("foo", tempFile.getName()) + .setModuleBaseDir(tempFile.getParentFile().toPath()) + .setCharset(StandardCharsets.UTF_8) + .build(); + FileMetadata.computeLineHashesForIssueTracking(f, new FileMetadata.LineHashConsumer() { + + @Override + public void consume(int lineIdx, @Nullable byte[] hash) { + switch (lineIdx) { + case 1: + assertThat(Hex.encodeHexString(hash)).isEqualTo(md5Hex("foo")); + break; + case 2: + assertThat(Hex.encodeHexString(hash)).isEqualTo(md5Hex("bar")); + break; + case 3: + assertThat(Hex.encodeHexString(hash)).isEqualTo(md5Hex("baz")); + break; + default: + fail("Invalid line"); + } + } + }); + } + + @Test + public void dont_fail_on_empty_file() throws Exception { + File tempFile = temp.newFile(); + FileUtils.write(tempFile, "", StandardCharsets.UTF_8, true); + + DefaultInputFile f = new TestInputFileBuilder("foo", tempFile.getName()) + .setModuleBaseDir(tempFile.getParentFile().toPath()) + .setCharset(StandardCharsets.UTF_8) + .build(); + FileMetadata.computeLineHashesForIssueTracking(f, new FileMetadata.LineHashConsumer() { + + @Override + public void consume(int lineIdx, @Nullable byte[] hash) { + switch (lineIdx) { + case 1: + assertThat(hash).isNull(); + break; + default: + fail("Invalid line"); + } + } + }); + } + + @Test + public void line_feed_is_included_into_hash() throws Exception { + File file1 = temp.newFile(); + FileUtils.write(file1, "foo\nbar\n", StandardCharsets.UTF_8, true); + + // same as file1, except an additional return carriage + File file1a = temp.newFile(); + FileUtils.write(file1a, "foo\r\nbar\n", StandardCharsets.UTF_8, true); + + File file2 = temp.newFile(); + FileUtils.write(file2, "foo\nbar", StandardCharsets.UTF_8, true); + + String hash1 = new FileMetadata().readMetadata(new FileInputStream(file1), StandardCharsets.UTF_8, file1.getName()).hash(); + String hash1a = new FileMetadata().readMetadata(new FileInputStream(file1a), StandardCharsets.UTF_8, file1a.getName()).hash(); + String hash2 = new FileMetadata().readMetadata(new FileInputStream(file2), StandardCharsets.UTF_8, file2.getName()).hash(); + + assertThat(hash1).isEqualTo(hash1a); + assertThat(hash1).isNotEqualTo(hash2); + } + + @Test + public void binary_file_with_unmappable_character() throws Exception { + File woff = new File(this.getClass().getResource("glyphicons-halflings-regular.woff").toURI()); + + Metadata metadata = new FileMetadata().readMetadata(new FileInputStream(woff), StandardCharsets.UTF_8, woff.getAbsolutePath()); + + assertThat(metadata.lines()).isEqualTo(135); + assertThat(metadata.nonBlankLines()).isEqualTo(133); + assertThat(metadata.hash()).isNotEmpty(); + + assertThat(logTester.logs(LoggerLevel.WARN).get(0)).contains("Invalid character encountered in file"); + assertThat(logTester.logs(LoggerLevel.WARN).get(0)).contains( + "glyphicons-halflings-regular.woff at line 1 for encoding UTF-8. Please fix file content or configure the encoding to be used using property 'sonar.sourceEncoding'."); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/MetadataTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/MetadataTest.java new file mode 100644 index 00000000000..49ecf8984d6 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/MetadataTest.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.api.impl.fs; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.sonar.api.impl.fs.Metadata; + +public class MetadataTest { + @Test + public void testRoundtrip() { + org.sonar.api.impl.fs.Metadata metadata = new Metadata(10, 20, "hash", new int[] {1, 3}, new int[] {2, 4}, 5); + assertThat(metadata.isEmpty()).isFalse(); + assertThat(metadata.lines()).isEqualTo(10); + assertThat(metadata.nonBlankLines()).isEqualTo(20); + assertThat(metadata.originalLineStartOffsets()).isEqualTo(new int[] {1, 3}); + assertThat(metadata.originalLineEndOffsets()).isEqualTo(new int[] {2, 4}); + assertThat(metadata.lastValidOffset()).isEqualTo(5); + assertThat(metadata.hash()).isEqualTo("hash"); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/PathPatternTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/PathPatternTest.java new file mode 100644 index 00000000000..c214469f081 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/PathPatternTest.java @@ -0,0 +1,110 @@ +/* + * 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.impl.fs; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.fs.IndexedFile; +import org.sonar.api.impl.fs.DefaultIndexedFile; +import org.sonar.api.impl.fs.PathPattern; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PathPatternTest { + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + private Path baseDir; + + @Before + public void setUp() throws IOException { + baseDir = temp.newFolder().toPath(); + } + + @Test + public void match_relative_path() { + org.sonar.api.impl.fs.PathPattern pattern = org.sonar.api.impl.fs.PathPattern.create("**/*Foo.java"); + assertThat(pattern.toString()).isEqualTo("**/*Foo.java"); + + IndexedFile indexedFile = new DefaultIndexedFile("ABCDE", baseDir, "src/main/java/org/MyFoo.java", null); + assertThat(pattern.match(indexedFile.path(), Paths.get(indexedFile.relativePath()))).isTrue(); + + // case sensitive by default + indexedFile = new DefaultIndexedFile("ABCDE", baseDir, "src/main/java/org/MyFoo.JAVA", null); + assertThat(pattern.match(indexedFile.path(), Paths.get(indexedFile.relativePath()))).isFalse(); + + indexedFile = new DefaultIndexedFile("ABCDE", baseDir, "src/main/java/org/Other.java", null); + assertThat(pattern.match(indexedFile.path(), Paths.get(indexedFile.relativePath()))).isFalse(); + } + + @Test + public void match_relative_path_and_insensitive_file_extension() throws Exception { + org.sonar.api.impl.fs.PathPattern pattern = org.sonar.api.impl.fs.PathPattern.create("**/*Foo.java"); + + IndexedFile indexedFile = new DefaultIndexedFile("ABCDE", baseDir, "src/main/java/org/MyFoo.JAVA", null); + assertThat(pattern.match(indexedFile.path(), Paths.get(indexedFile.relativePath()), false)).isTrue(); + + indexedFile = new DefaultIndexedFile("ABCDE", baseDir, "src/main/java/org/Other.java", null); + assertThat(pattern.match(indexedFile.path(), Paths.get(indexedFile.relativePath()), false)).isFalse(); + } + + @Test + public void match_absolute_path() throws Exception { + org.sonar.api.impl.fs.PathPattern pattern = org.sonar.api.impl.fs.PathPattern.create("file:**/src/main/**Foo.java"); + assertThat(pattern.toString()).isEqualTo("file:**/src/main/**Foo.java"); + + IndexedFile indexedFile = new DefaultIndexedFile("ABCDE", baseDir, "src/main/java/org/MyFoo.java", null); + assertThat(pattern.match(indexedFile.path(), Paths.get(indexedFile.relativePath()))).isTrue(); + + // case sensitive by default + indexedFile = new DefaultIndexedFile("ABCDE", baseDir, "src/main/java/org/MyFoo.JAVA", null); + assertThat(pattern.match(indexedFile.path(), Paths.get(indexedFile.relativePath()))).isFalse(); + + indexedFile = new DefaultIndexedFile("ABCDE", baseDir, "src/main/java/org/Other.java", null); + assertThat(pattern.match(indexedFile.path(), Paths.get(indexedFile.relativePath()))).isFalse(); + } + + @Test + public void match_absolute_path_and_insensitive_file_extension() throws Exception { + org.sonar.api.impl.fs.PathPattern pattern = org.sonar.api.impl.fs.PathPattern.create("file:**/src/main/**Foo.java"); + assertThat(pattern.toString()).isEqualTo("file:**/src/main/**Foo.java"); + + IndexedFile indexedFile = new DefaultIndexedFile("ABCDE", baseDir, "src/main/java/org/MyFoo.JAVA", null); + assertThat(pattern.match(indexedFile.path(), Paths.get(indexedFile.relativePath()), false)).isTrue(); + + indexedFile = new DefaultIndexedFile("ABCDE", baseDir, "src/main/java/org/Other.JAVA", null); + assertThat(pattern.match(indexedFile.path(), Paths.get(indexedFile.relativePath()), false)).isFalse(); + } + + @Test + public void create_array_of_patterns() { + org.sonar.api.impl.fs.PathPattern[] patterns = PathPattern.create(new String[] { + "**/src/main/**Foo.java", + "file:**/src/main/**Bar.java" + }); + assertThat(patterns).hasSize(2); + assertThat(patterns[0].toString()).isEqualTo("**/src/main/**Foo.java"); + assertThat(patterns[1].toString()).isEqualTo("file:**/src/main/**Bar.java"); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/TestInputFileBuilderTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/TestInputFileBuilderTest.java new file mode 100644 index 00000000000..4fb37a2fd77 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/TestInputFileBuilderTest.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.api.impl.fs; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.apache.commons.io.IOUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.fs.InputFile.Status; +import org.sonar.api.batch.fs.InputFile.Type; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestInputFileBuilderTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void setContent() throws IOException { + DefaultInputFile file = TestInputFileBuilder.create("module", "invalidPath") + .setContents("my content") + .setCharset(StandardCharsets.UTF_8) + .build(); + assertThat(file.contents()).isEqualTo("my content"); + assertThat(IOUtils.toString(file.inputStream())).isEqualTo("my content"); + } + + @Test + public void testGetters() { + DefaultInputFile file = TestInputFileBuilder.create("module", new File("baseDir"), new File("baseDir", "path")) + .setStatus(Status.SAME) + .setType(Type.MAIN) + .build(); + + assertThat(file.type()).isEqualTo(Type.MAIN); + assertThat(file.status()).isEqualTo(Status.SAME); + assertThat(file.isPublished()).isTrue(); + assertThat(file.type()).isEqualTo(Type.MAIN); + assertThat(file.relativePath()).isEqualTo("path"); + assertThat(file.absolutePath()).isEqualTo("baseDir/path"); + + } + + @Test + public void testCreateInputModule() throws IOException { + File baseDir = temp.newFolder(); + AbstractProjectOrModule module = TestInputFileBuilder.newDefaultInputModule("key", baseDir); + assertThat(module.key()).isEqualTo("key"); + assertThat(module.getBaseDir()).isEqualTo(baseDir.toPath()); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/charhandler/IntArrayListTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/charhandler/IntArrayListTest.java new file mode 100644 index 00000000000..50c37d8a565 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/charhandler/IntArrayListTest.java @@ -0,0 +1,55 @@ +/* + * 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.impl.fs.charhandler; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class IntArrayListTest { + + @Test + public void addElements() { + IntArrayList list = new IntArrayList(); + assertThat(list.trimAndGet()).isEmpty(); + list.add(1); + list.add(2); + assertThat(list.trimAndGet()).containsExactly(1, 2); + } + + @Test + public void trimIfNeeded() { + IntArrayList list = new IntArrayList(); + list.add(1); + list.add(2); + assertThat(list.trimAndGet()).isSameAs(list.trimAndGet()); + } + + @Test + public void grow() { + // Default capacity is 10 + IntArrayList list = new IntArrayList(); + for (int i = 1; i <= 11; i++) { + list.add(i); + } + assertThat(list.trimAndGet()).hasSize(11); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/AndPredicateTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/AndPredicateTest.java new file mode 100644 index 00000000000..e02d5e433f4 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/AndPredicateTest.java @@ -0,0 +1,116 @@ +/* + * 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.impl.fs.predicates; + +import java.util.Arrays; +import org.junit.Test; +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.impl.fs.PathPattern; +import org.sonar.api.impl.fs.TestInputFileBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AndPredicateTest { + + @Test + public void flattenNestedAnd() { + PathPatternPredicate pathPatternPredicate1 = new PathPatternPredicate(PathPattern.create("foo1/**")); + PathPatternPredicate pathPatternPredicate2 = new PathPatternPredicate(PathPattern.create("foo2/**")); + PathPatternPredicate pathPatternPredicate3 = new PathPatternPredicate(PathPattern.create("foo3/**")); + FilePredicate andPredicate = AndPredicate.create(Arrays.asList(pathPatternPredicate1, + AndPredicate.create(Arrays.asList(pathPatternPredicate2, pathPatternPredicate3)))); + assertThat(((AndPredicate) andPredicate).predicates()).containsExactly(pathPatternPredicate1, pathPatternPredicate2, pathPatternPredicate3); + } + + @Test + public void applyPredicates() { + PathPatternPredicate pathPatternPredicate1 = new PathPatternPredicate(PathPattern.create("foo/**")); + PathPatternPredicate pathPatternPredicate2 = new PathPatternPredicate(PathPattern.create("foo/file1")); + PathPatternPredicate pathPatternPredicate3 = new PathPatternPredicate(PathPattern.create("**")); + FilePredicate andPredicate = AndPredicate.create(Arrays.asList(pathPatternPredicate1, + AndPredicate.create(Arrays.asList(pathPatternPredicate2, pathPatternPredicate3)))); + + InputFile file1 = TestInputFileBuilder.create("module", "foo/file1").build(); + InputFile file2 = TestInputFileBuilder.create("module", "foo2/file1").build(); + InputFile file3 = TestInputFileBuilder.create("module", "foo/file2").build(); + + assertThat(andPredicate.apply(file1)).isTrue(); + assertThat(andPredicate.apply(file2)).isFalse(); + assertThat(andPredicate.apply(file3)).isFalse(); + } + + @Test + public void filterIndex() { + PathPatternPredicate pathPatternPredicate1 = new PathPatternPredicate(PathPattern.create("foo/**")); + PathPatternPredicate pathPatternPredicate2 = new PathPatternPredicate(PathPattern.create("foo/file1")); + PathPatternPredicate pathPatternPredicate3 = new PathPatternPredicate(PathPattern.create("**")); + + InputFile file1 = TestInputFileBuilder.create("module", "foo/file1").build(); + InputFile file2 = TestInputFileBuilder.create("module", "foo2/file1").build(); + InputFile file3 = TestInputFileBuilder.create("module", "foo/file2").build(); + + FileSystem.Index index = mock(FileSystem.Index.class); + when(index.inputFiles()).thenReturn(Arrays.asList(file1, file2, file3)); + + OptimizedFilePredicate andPredicate = (OptimizedFilePredicate) AndPredicate.create(Arrays.asList(pathPatternPredicate1, + AndPredicate.create(Arrays.asList(pathPatternPredicate2, pathPatternPredicate3)))); + + assertThat(andPredicate.get(index)).containsOnly(file1); + } + + @Test + public void sortPredicatesByPriority() { + PathPatternPredicate pathPatternPredicate1 = new PathPatternPredicate(PathPattern.create("foo1/**")); + PathPatternPredicate pathPatternPredicate2 = new PathPatternPredicate(PathPattern.create("foo2/**")); + RelativePathPredicate relativePathPredicate = new RelativePathPredicate("foo"); + FilePredicate andPredicate = AndPredicate.create(Arrays.asList(pathPatternPredicate1, + relativePathPredicate, pathPatternPredicate2)); + assertThat(((AndPredicate) andPredicate).predicates()).containsExactly(relativePathPredicate, pathPatternPredicate1, pathPatternPredicate2); + } + + @Test + public void simplifyAndExpressionsWhenEmpty() { + FilePredicate andPredicate = AndPredicate.create(Arrays.asList()); + assertThat(andPredicate).isEqualTo(TruePredicate.TRUE); + } + + @Test + public void simplifyAndExpressionsWhenTrue() { + PathPatternPredicate pathPatternPredicate1 = new PathPatternPredicate(PathPattern.create("foo1/**")); + PathPatternPredicate pathPatternPredicate2 = new PathPatternPredicate(PathPattern.create("foo2/**")); + FilePredicate andPredicate = AndPredicate.create(Arrays.asList(pathPatternPredicate1, + TruePredicate.TRUE, pathPatternPredicate2)); + assertThat(((AndPredicate) andPredicate).predicates()).containsExactly(pathPatternPredicate1, pathPatternPredicate2); + } + + @Test + public void simplifyAndExpressionsWhenFalse() { + PathPatternPredicate pathPatternPredicate1 = new PathPatternPredicate(PathPattern.create("foo1/**")); + PathPatternPredicate pathPatternPredicate2 = new PathPatternPredicate(PathPattern.create("foo2/**")); + FilePredicate andPredicate = AndPredicate.create(Arrays.asList(pathPatternPredicate1, + FalsePredicate.FALSE, pathPatternPredicate2)); + assertThat(andPredicate).isEqualTo(FalsePredicate.FALSE); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/DefaultFilePredicatesTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/DefaultFilePredicatesTest.java new file mode 100644 index 00000000000..6d9cd4eafed --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/DefaultFilePredicatesTest.java @@ -0,0 +1,245 @@ +/* + * 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.impl.fs.predicates; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +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.api.impl.fs.TestInputFileBuilder; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DefaultFilePredicatesTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private Path moduleBasePath; + + @Before + public void setUp() throws IOException { + moduleBasePath = temp.newFolder().toPath(); + } + + InputFile javaFile; + FilePredicates predicates; + + @Before + public void before() throws IOException { + predicates = new DefaultFilePredicates(temp.newFolder().toPath()); + javaFile = new TestInputFileBuilder("foo", "src/main/java/struts/Action.java") + .setModuleBaseDir(moduleBasePath) + .setLanguage("java") + .setStatus(Status.SAME) + .build(); + + } + + @Test + public void all() { + assertThat(predicates.all().apply(javaFile)).isTrue(); + } + + @Test + public void none() { + assertThat(predicates.none().apply(javaFile)).isFalse(); + } + + @Test + public void matches_inclusion_pattern() { + assertThat(predicates.matchesPathPattern("src/main/**/Action.java").apply(javaFile)).isTrue(); + assertThat(predicates.matchesPathPattern("Action.java").apply(javaFile)).isFalse(); + assertThat(predicates.matchesPathPattern("src/**/*.php").apply(javaFile)).isFalse(); + } + + @Test + public void matches_inclusion_patterns() { + assertThat(predicates.matchesPathPatterns(new String[] {"src/other/**.java", "src/main/**/Action.java"}).apply(javaFile)).isTrue(); + assertThat(predicates.matchesPathPatterns(new String[] {}).apply(javaFile)).isTrue(); + assertThat(predicates.matchesPathPatterns(new String[] {"src/other/**.java", "src/**/*.php"}).apply(javaFile)).isFalse(); + } + + @Test + public void does_not_match_exclusion_pattern() { + assertThat(predicates.doesNotMatchPathPattern("src/main/**/Action.java").apply(javaFile)).isFalse(); + assertThat(predicates.doesNotMatchPathPattern("Action.java").apply(javaFile)).isTrue(); + assertThat(predicates.doesNotMatchPathPattern("src/**/*.php").apply(javaFile)).isTrue(); + } + + @Test + public void does_not_match_exclusion_patterns() { + assertThat(predicates.doesNotMatchPathPatterns(new String[] {}).apply(javaFile)).isTrue(); + assertThat(predicates.doesNotMatchPathPatterns(new String[] {"src/other/**.java", "src/**/*.php"}).apply(javaFile)).isTrue(); + assertThat(predicates.doesNotMatchPathPatterns(new String[] {"src/other/**.java", "src/main/**/Action.java"}).apply(javaFile)).isFalse(); + } + + @Test + public void has_relative_path() { + assertThat(predicates.hasRelativePath("src/main/java/struts/Action.java").apply(javaFile)).isTrue(); + assertThat(predicates.hasRelativePath("src/main/java/struts/Other.java").apply(javaFile)).isFalse(); + + // path is normalized + assertThat(predicates.hasRelativePath("src/main/java/../java/struts/Action.java").apply(javaFile)).isTrue(); + + assertThat(predicates.hasRelativePath("src\\main\\java\\struts\\Action.java").apply(javaFile)).isTrue(); + assertThat(predicates.hasRelativePath("src\\main\\java\\struts\\Other.java").apply(javaFile)).isFalse(); + assertThat(predicates.hasRelativePath("src\\main\\java\\struts\\..\\struts\\Action.java").apply(javaFile)).isTrue(); + } + + @Test + public void has_absolute_path() throws Exception { + String path = javaFile.file().getAbsolutePath(); + assertThat(predicates.hasAbsolutePath(path).apply(javaFile)).isTrue(); + assertThat(predicates.hasAbsolutePath(path.replaceAll("/", "\\\\")).apply(javaFile)).isTrue(); + + assertThat(predicates.hasAbsolutePath(temp.newFile().getAbsolutePath()).apply(javaFile)).isFalse(); + assertThat(predicates.hasAbsolutePath("src/main/java/struts/Action.java").apply(javaFile)).isFalse(); + } + + @Test + public void has_uri() throws Exception { + URI uri = javaFile.uri(); + assertThat(predicates.hasURI(uri).apply(javaFile)).isTrue(); + + assertThat(predicates.hasURI(temp.newFile().toURI()).apply(javaFile)).isFalse(); + } + + @Test + public void has_path() throws Exception { + // is relative path + assertThat(predicates.hasPath("src/main/java/struts/Action.java").apply(javaFile)).isTrue(); + assertThat(predicates.hasPath("src/main/java/struts/Other.java").apply(javaFile)).isFalse(); + + // is absolute path + String path = javaFile.file().getAbsolutePath(); + assertThat(predicates.hasAbsolutePath(path).apply(javaFile)).isTrue(); + assertThat(predicates.hasPath(temp.newFile().getAbsolutePath()).apply(javaFile)).isFalse(); + } + + @Test + public void is_file() throws Exception { + // relative file + assertThat(predicates.is(new File(javaFile.relativePath())).apply(javaFile)).isTrue(); + + // absolute file + assertThat(predicates.is(javaFile.file()).apply(javaFile)).isTrue(); + assertThat(predicates.is(javaFile.file().getAbsoluteFile()).apply(javaFile)).isTrue(); + assertThat(predicates.is(new File(javaFile.file().toURI())).apply(javaFile)).isTrue(); + assertThat(predicates.is(temp.newFile()).apply(javaFile)).isFalse(); + } + + @Test + public void has_language() { + assertThat(predicates.hasLanguage("java").apply(javaFile)).isTrue(); + assertThat(predicates.hasLanguage("php").apply(javaFile)).isFalse(); + } + + @Test + public void has_languages() { + assertThat(predicates.hasLanguages(Arrays.asList("java", "php")).apply(javaFile)).isTrue(); + assertThat(predicates.hasLanguages(Arrays.asList("cobol", "php")).apply(javaFile)).isFalse(); + assertThat(predicates.hasLanguages(Collections.<String>emptyList()).apply(javaFile)).isTrue(); + } + + @Test + public void has_type() { + assertThat(predicates.hasType(InputFile.Type.MAIN).apply(javaFile)).isTrue(); + assertThat(predicates.hasType(InputFile.Type.TEST).apply(javaFile)).isFalse(); + } + + @Test + public void has_status() { + assertThat(predicates.hasAnyStatus().apply(javaFile)).isTrue(); + assertThat(predicates.hasStatus(InputFile.Status.SAME).apply(javaFile)).isTrue(); + assertThat(predicates.hasStatus(InputFile.Status.ADDED).apply(javaFile)).isFalse(); + } + + @Test + public void not() { + assertThat(predicates.not(predicates.hasType(InputFile.Type.MAIN)).apply(javaFile)).isFalse(); + assertThat(predicates.not(predicates.hasType(InputFile.Type.TEST)).apply(javaFile)).isTrue(); + } + + @Test + public void and() { + // empty + assertThat(predicates.and().apply(javaFile)).isTrue(); + assertThat(predicates.and(new FilePredicate[0]).apply(javaFile)).isTrue(); + assertThat(predicates.and(Collections.<FilePredicate>emptyList()).apply(javaFile)).isTrue(); + + // two arguments + assertThat(predicates.and(predicates.all(), predicates.all()).apply(javaFile)).isTrue(); + assertThat(predicates.and(predicates.all(), predicates.none()).apply(javaFile)).isFalse(); + assertThat(predicates.and(predicates.none(), predicates.all()).apply(javaFile)).isFalse(); + + // collection + assertThat(predicates.and(Arrays.asList(predicates.all(), predicates.all())).apply(javaFile)).isTrue(); + assertThat(predicates.and(Arrays.asList(predicates.all(), predicates.none())).apply(javaFile)).isFalse(); + + // array + assertThat(predicates.and(new FilePredicate[] {predicates.all(), predicates.all()}).apply(javaFile)).isTrue(); + assertThat(predicates.and(new FilePredicate[] {predicates.all(), predicates.none()}).apply(javaFile)).isFalse(); + } + + @Test + public void or() { + // empty + assertThat(predicates.or().apply(javaFile)).isTrue(); + assertThat(predicates.or(new FilePredicate[0]).apply(javaFile)).isTrue(); + assertThat(predicates.or(Collections.<FilePredicate>emptyList()).apply(javaFile)).isTrue(); + + // two arguments + assertThat(predicates.or(predicates.all(), predicates.all()).apply(javaFile)).isTrue(); + assertThat(predicates.or(predicates.all(), predicates.none()).apply(javaFile)).isTrue(); + assertThat(predicates.or(predicates.none(), predicates.all()).apply(javaFile)).isTrue(); + assertThat(predicates.or(predicates.none(), predicates.none()).apply(javaFile)).isFalse(); + + // collection + assertThat(predicates.or(Arrays.asList(predicates.all(), predicates.all())).apply(javaFile)).isTrue(); + assertThat(predicates.or(Arrays.asList(predicates.all(), predicates.none())).apply(javaFile)).isTrue(); + assertThat(predicates.or(Arrays.asList(predicates.none(), predicates.none())).apply(javaFile)).isFalse(); + + // array + assertThat(predicates.or(new FilePredicate[] {predicates.all(), predicates.all()}).apply(javaFile)).isTrue(); + assertThat(predicates.or(new FilePredicate[] {predicates.all(), predicates.none()}).apply(javaFile)).isTrue(); + assertThat(predicates.or(new FilePredicate[] {predicates.none(), predicates.none()}).apply(javaFile)).isFalse(); + } + + @Test + public void hasFilename() { + assertThat(predicates.hasFilename("Action.java").apply(javaFile)).isTrue(); + } + + @Test + public void hasExtension() { + assertThat(predicates.hasExtension("java").apply(javaFile)).isTrue(); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/FileExtensionPredicateTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/FileExtensionPredicateTest.java new file mode 100644 index 00000000000..0f60f6bc36d --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/FileExtensionPredicateTest.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.api.impl.fs.predicates; + +import java.io.IOException; +import org.junit.Test; +import org.sonar.api.batch.fs.InputFile; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.api.impl.fs.predicates.FileExtensionPredicate.getExtension; + +public class FileExtensionPredicateTest { + + @Test + public void should_match_correct_extension() throws IOException { + FileExtensionPredicate predicate = new FileExtensionPredicate("bat"); + assertThat(predicate.apply(mockWithName("prog.bat"))).isTrue(); + assertThat(predicate.apply(mockWithName("prog.bat.bat"))).isTrue(); + } + + @Test + public void should_not_match_incorrect_extension() throws IOException { + FileExtensionPredicate predicate = new FileExtensionPredicate("bat"); + assertThat(predicate.apply(mockWithName("prog.batt"))).isFalse(); + assertThat(predicate.apply(mockWithName("prog.abat"))).isFalse(); + assertThat(predicate.apply(mockWithName("prog."))).isFalse(); + assertThat(predicate.apply(mockWithName("prog.bat."))).isFalse(); + assertThat(predicate.apply(mockWithName("prog.bat.batt"))).isFalse(); + assertThat(predicate.apply(mockWithName("prog"))).isFalse(); + } + + @Test + public void should_match_correct_extension_case_insensitively() throws IOException { + FileExtensionPredicate predicate = new FileExtensionPredicate("jAVa"); + assertThat(predicate.apply(mockWithName("Program.java"))).isTrue(); + assertThat(predicate.apply(mockWithName("Program.JAVA"))).isTrue(); + assertThat(predicate.apply(mockWithName("Program.Java"))).isTrue(); + assertThat(predicate.apply(mockWithName("Program.JaVa"))).isTrue(); + } + + @Test + public void test_empty_extension() { + assertThat(getExtension("prog")).isEmpty(); + assertThat(getExtension("prog.")).isEmpty(); + assertThat(getExtension(".")).isEmpty(); + } + + private InputFile mockWithName(String filename) throws IOException { + InputFile inputFile = mock(InputFile.class); + when(inputFile.filename()).thenReturn(filename); + return inputFile; + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/FilenamePredicateTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/FilenamePredicateTest.java new file mode 100644 index 00000000000..052ff51c015 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/FilenamePredicateTest.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.api.impl.fs.predicates; + +import java.io.IOException; +import java.util.Collections; +import org.junit.Test; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class FilenamePredicateTest { + @Test + public void should_match_file_by_filename() throws IOException { + String filename = "some name"; + InputFile inputFile = mock(InputFile.class); + when(inputFile.filename()).thenReturn(filename); + + assertThat(new FilenamePredicate(filename).apply(inputFile)).isTrue(); + } + + @Test + public void should_not_match_file_by_different_filename() throws IOException { + String filename = "some name"; + InputFile inputFile = mock(InputFile.class); + when(inputFile.filename()).thenReturn(filename + "x"); + + assertThat(new FilenamePredicate(filename).apply(inputFile)).isFalse(); + } + + @Test + public void should_find_matching_file_in_index() throws IOException { + String filename = "some name"; + InputFile inputFile = mock(InputFile.class); + when(inputFile.filename()).thenReturn(filename); + + FileSystem.Index index = mock(FileSystem.Index.class); + when(index.getFilesByName(filename)).thenReturn(Collections.singleton(inputFile)); + + assertThat(new FilenamePredicate(filename).get(index)).containsOnly(inputFile); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/OrPredicateTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/OrPredicateTest.java new file mode 100644 index 00000000000..489d6366543 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/OrPredicateTest.java @@ -0,0 +1,66 @@ +/* + * 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.impl.fs.predicates; + +import org.junit.Test; +import org.sonar.api.batch.fs.FilePredicate; + +import java.util.Arrays; +import org.sonar.api.impl.fs.PathPattern; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OrPredicateTest { + + @Test + public void flattenNestedOr() { + PathPatternPredicate pathPatternPredicate1 = new PathPatternPredicate(PathPattern.create("foo1/**")); + PathPatternPredicate pathPatternPredicate2 = new PathPatternPredicate(PathPattern.create("foo2/**")); + PathPatternPredicate pathPatternPredicate3 = new PathPatternPredicate(PathPattern.create("foo3/**")); + FilePredicate orPredicate = OrPredicate.create(Arrays.asList(pathPatternPredicate1, + OrPredicate.create(Arrays.asList(pathPatternPredicate2, pathPatternPredicate3)))); + assertThat(((OrPredicate) orPredicate).predicates()).containsExactly(pathPatternPredicate1, pathPatternPredicate2, pathPatternPredicate3); + } + + @Test + public void simplifyOrExpressionsWhenEmpty() { + FilePredicate orPredicate = OrPredicate.create(Arrays.asList()); + assertThat(orPredicate).isEqualTo(TruePredicate.TRUE); + } + + @Test + public void simplifyOrExpressionsWhenFalse() { + PathPatternPredicate pathPatternPredicate1 = new PathPatternPredicate(PathPattern.create("foo1/**")); + PathPatternPredicate pathPatternPredicate2 = new PathPatternPredicate(PathPattern.create("foo2/**")); + FilePredicate andPredicate = OrPredicate.create(Arrays.asList(pathPatternPredicate1, + FalsePredicate.FALSE, pathPatternPredicate2)); + assertThat(((OrPredicate) andPredicate).predicates()).containsExactly(pathPatternPredicate1, pathPatternPredicate2); + } + + @Test + public void simplifyAndExpressionsWhenTrue() { + PathPatternPredicate pathPatternPredicate1 = new PathPatternPredicate(PathPattern.create("foo1/**")); + PathPatternPredicate pathPatternPredicate2 = new PathPatternPredicate(PathPattern.create("foo2/**")); + FilePredicate andPredicate = OrPredicate.create(Arrays.asList(pathPatternPredicate1, + TruePredicate.TRUE, pathPatternPredicate2)); + assertThat(andPredicate).isEqualTo(TruePredicate.TRUE); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/RelativePathPredicateTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/RelativePathPredicateTest.java new file mode 100644 index 00000000000..e7e2efd9571 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/fs/predicates/RelativePathPredicateTest.java @@ -0,0 +1,53 @@ +/* + * 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.impl.fs.predicates; + +import org.junit.Test; +import org.sonar.api.batch.fs.InputFile; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RelativePathPredicateTest { + @Test + public void returns_false_when_path_is_invalid() { + RelativePathPredicate predicate = new RelativePathPredicate(".."); + InputFile inputFile = mock(InputFile.class); + when(inputFile.relativePath()).thenReturn("path"); + assertThat(predicate.apply(inputFile)).isFalse(); + } + + @Test + public void returns_true_if_matches() { + RelativePathPredicate predicate = new RelativePathPredicate("path"); + InputFile inputFile = mock(InputFile.class); + when(inputFile.relativePath()).thenReturn("path"); + assertThat(predicate.apply(inputFile)).isTrue(); + } + + @Test + public void returns_false_if_doesnt_match() { + RelativePathPredicate predicate = new RelativePathPredicate("path1"); + InputFile inputFile = mock(InputFile.class); + when(inputFile.relativePath()).thenReturn("path2"); + assertThat(predicate.apply(inputFile)).isFalse(); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/rule/DefaultRulesTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/rule/DefaultRulesTest.java new file mode 100644 index 00000000000..c55551687b9 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/rule/DefaultRulesTest.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.api.impl.rule; + +import java.util.LinkedList; +import java.util.List; +import org.junit.Test; +import org.sonar.api.batch.rule.NewRule; +import org.sonar.api.impl.rule.DefaultRules; +import org.sonar.api.rule.RuleKey; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DefaultRulesTest { + @Test + public void testRepeatedInternalKey() { + List<NewRule> newRules = new LinkedList<>(); + newRules.add(createRule("key1", "repo", "internal")); + newRules.add(createRule("key2", "repo", "internal")); + + DefaultRules rules = new DefaultRules(newRules); + assertThat(rules.findByInternalKey("repo", "internal")).hasSize(2); + assertThat(rules.find(RuleKey.of("repo", "key1"))).isNotNull(); + assertThat(rules.find(RuleKey.of("repo", "key2"))).isNotNull(); + assertThat(rules.findByRepository("repo")).hasSize(2); + } + + @Test + public void testNonExistingKey() { + List<NewRule> newRules = new LinkedList<>(); + newRules.add(createRule("key1", "repo", "internal")); + newRules.add(createRule("key2", "repo", "internal")); + + DefaultRules rules = new DefaultRules(newRules); + assertThat(rules.findByInternalKey("xx", "xx")).hasSize(0); + assertThat(rules.find(RuleKey.of("xxx", "xx"))).isNull(); + assertThat(rules.findByRepository("xxxx")).hasSize(0); + } + + @Test + public void testRepeatedRule() { + List<NewRule> newRules = new LinkedList<>(); + newRules.add(createRule("key", "repo", "internal")); + newRules.add(createRule("key", "repo", "internal")); + + DefaultRules rules = new DefaultRules(newRules); + assertThat(rules.find(RuleKey.of("repo", "key"))).isNotNull(); + } + + private NewRule createRule(String key, String repo, String internalKey) { + RuleKey ruleKey = RuleKey.of(repo, key); + NewRule newRule = new NewRule(ruleKey); + newRule.setInternalKey(internalKey); + + return newRule; + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/rule/RulesBuilderTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/rule/RulesBuilderTest.java new file mode 100644 index 00000000000..1330f906502 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/rule/RulesBuilderTest.java @@ -0,0 +1,114 @@ +/* + * 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.impl.rule; + +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.batch.rule.NewRule; +import org.sonar.api.batch.rule.Rule; +import org.sonar.api.batch.rule.Rules; +import org.sonar.api.impl.rule.RulesBuilder; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rule.RuleStatus; +import org.sonar.api.rule.Severity; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RulesBuilderTest { + @org.junit.Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void no_rules() { + RulesBuilder builder = new RulesBuilder(); + Rules rules = builder.build(); + assertThat(rules.findAll()).isEmpty(); + } + + @Test + public void build_rules() { + RulesBuilder builder = new RulesBuilder(); + NewRule newSquid1 = builder.add(RuleKey.of("squid", "S0001")); + newSquid1.setName("Detect bug"); + newSquid1.setDescription("Detect potential bug"); + newSquid1.setInternalKey("foo=bar"); + newSquid1.setSeverity(org.sonar.api.rule.Severity.CRITICAL); + newSquid1.setStatus(RuleStatus.BETA); + newSquid1.addParam("min"); + newSquid1.addParam("max").setDescription("Maximum"); + // most simple rule + builder.add(RuleKey.of("squid", "S0002")); + builder.add(RuleKey.of("findbugs", "NPE")); + + Rules rules = builder.build(); + + assertThat(rules.findAll()).hasSize(3); + assertThat(rules.findByRepository("squid")).hasSize(2); + assertThat(rules.findByRepository("findbugs")).hasSize(1); + assertThat(rules.findByRepository("unknown")).isEmpty(); + + Rule squid1 = rules.find(RuleKey.of("squid", "S0001")); + assertThat(squid1.key().repository()).isEqualTo("squid"); + assertThat(squid1.key().rule()).isEqualTo("S0001"); + assertThat(squid1.name()).isEqualTo("Detect bug"); + assertThat(squid1.description()).isEqualTo("Detect potential bug"); + assertThat(squid1.internalKey()).isEqualTo("foo=bar"); + assertThat(squid1.status()).isEqualTo(RuleStatus.BETA); + assertThat(squid1.severity()).isEqualTo(org.sonar.api.rule.Severity.CRITICAL); + assertThat(squid1.params()).hasSize(2); + assertThat(squid1.param("min").key()).isEqualTo("min"); + assertThat(squid1.param("min").description()).isNull(); + assertThat(squid1.param("max").key()).isEqualTo("max"); + assertThat(squid1.param("max").description()).isEqualTo("Maximum"); + + Rule squid2 = rules.find(RuleKey.of("squid", "S0002")); + assertThat(squid2.key().repository()).isEqualTo("squid"); + assertThat(squid2.key().rule()).isEqualTo("S0002"); + assertThat(squid2.description()).isNull(); + assertThat(squid2.internalKey()).isNull(); + assertThat(squid2.status()).isEqualTo(RuleStatus.defaultStatus()); + assertThat(squid2.severity()).isEqualTo(Severity.defaultSeverity()); + assertThat(squid2.params()).isEmpty(); + } + + @Test + public void fail_to_add_twice_the_same_rule() { + RulesBuilder builder = new RulesBuilder(); + builder.add(RuleKey.of("squid", "S0001")); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Rule 'squid:S0001' already exists"); + + builder.add(RuleKey.of("squid", "S0001")); + } + + @Test + public void fail_to_add_twice_the_same_param() { + RulesBuilder builder = new RulesBuilder(); + NewRule newRule = builder.add(RuleKey.of("squid", "S0001")); + newRule.addParam("min"); + newRule.addParam("max"); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Parameter 'min' already exists on rule 'squid:S0001'"); + + newRule.addParam("min"); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultAdHocRuleTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultAdHocRuleTest.java new file mode 100644 index 00000000000..fd766c2739b --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultAdHocRuleTest.java @@ -0,0 +1,156 @@ +/* + * 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.impl.sensor; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.batch.rule.Severity; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.batch.sensor.rule.NewAdHocRule; +import org.sonar.api.rules.RuleType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class DefaultAdHocRuleTest { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void store() { + SensorStorage storage = mock(SensorStorage.class); + DefaultAdHocRule rule = new DefaultAdHocRule(storage) + .engineId("engine") + .ruleId("ruleId") + .name("name") + .description("desc") + .severity(Severity.BLOCKER) + .type(RuleType.CODE_SMELL); + rule.save(); + + assertThat(rule.engineId()).isEqualTo("engine"); + assertThat(rule.ruleId()).isEqualTo("ruleId"); + assertThat(rule.name()).isEqualTo("name"); + assertThat(rule.description()).isEqualTo("desc"); + assertThat(rule.severity()).isEqualTo(Severity.BLOCKER); + assertThat(rule.type()).isEqualTo(RuleType.CODE_SMELL); + + verify(storage).store(any(DefaultAdHocRule.class)); + } + + + @Test + public void description_is_optional() { + SensorStorage storage = mock(SensorStorage.class); + new DefaultAdHocRule(storage) + .engineId("engine") + .ruleId("ruleId") + .name("name") + .severity(Severity.BLOCKER) + .type(RuleType.CODE_SMELL) + .save(); + + verify(storage).store(any(DefaultAdHocRule.class)); + } + + @Test + public void fail_to_store_if_no_engine_id() { + SensorStorage storage = mock(SensorStorage.class); + NewAdHocRule rule = new DefaultAdHocRule(storage) + .engineId(" ") + .ruleId("ruleId") + .name("name") + .description("desc") + .severity(Severity.BLOCKER) + .type(RuleType.CODE_SMELL); + + exception.expect(IllegalStateException.class); + exception.expectMessage("Engine id is mandatory"); + rule.save(); + } + + @Test + public void fail_to_store_if_no_rule_id() { + SensorStorage storage = mock(SensorStorage.class); + NewAdHocRule rule = new DefaultAdHocRule(storage) + .engineId("engine") + .ruleId(" ") + .name("name") + .description("desc") + .severity(Severity.BLOCKER) + .type(RuleType.CODE_SMELL); + + exception.expect(IllegalStateException.class); + exception.expectMessage("Rule id is mandatory"); + rule.save(); + } + + @Test + public void fail_to_store_if_no_name() { + SensorStorage storage = mock(SensorStorage.class); + NewAdHocRule rule = new DefaultAdHocRule(storage) + .engineId("engine") + .ruleId("ruleId") + .name(" ") + .description("desc") + .severity(Severity.BLOCKER) + .type(RuleType.CODE_SMELL); + + exception.expect(IllegalStateException.class); + exception.expectMessage("Name is mandatory"); + rule.save(); + } + + + @Test + public void fail_to_store_if_no_severity() { + SensorStorage storage = mock(SensorStorage.class); + NewAdHocRule rule = new DefaultAdHocRule(storage) + .engineId("engine") + .ruleId("ruleId") + .name("name") + .description("desc") + .type(RuleType.CODE_SMELL); + + exception.expect(IllegalStateException.class); + exception.expectMessage("Severity is mandatory"); + rule.save(); + } + + @Test + public void fail_to_store_if_no_type() { + SensorStorage storage = mock(SensorStorage.class); + NewAdHocRule rule = new DefaultAdHocRule(storage) + .engineId("engine") + .ruleId("ruleId") + .name("name") + .description("desc") + .severity(Severity.BLOCKER); + + exception.expect(IllegalStateException.class); + exception.expectMessage("Type is mandatory"); + rule.save(); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultAnalysisErrorTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultAnalysisErrorTest.java new file mode 100644 index 00000000000..9f6be9d2d9f --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultAnalysisErrorTest.java @@ -0,0 +1,114 @@ +/* + * 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.impl.sensor; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.TextPointer; +import org.sonar.api.batch.sensor.error.NewAnalysisError; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.impl.fs.DefaultTextPointer; +import org.sonar.api.impl.fs.TestInputFileBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class DefaultAnalysisErrorTest { + private InputFile inputFile; + private SensorStorage storage; + private TextPointer textPointer; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Before + public void setUp() { + inputFile = new TestInputFileBuilder("module1", "src/File.java").build(); + textPointer = new DefaultTextPointer(5, 2); + storage = mock(SensorStorage.class); + } + + @Test + public void test_analysis_error() { + DefaultAnalysisError analysisError = new DefaultAnalysisError(storage); + analysisError.onFile(inputFile) + .at(textPointer) + .message("msg"); + + assertThat(analysisError.location()).isEqualTo(textPointer); + assertThat(analysisError.message()).isEqualTo("msg"); + assertThat(analysisError.inputFile()).isEqualTo(inputFile); + } + + @Test + public void test_save() { + DefaultAnalysisError analysisError = new DefaultAnalysisError(storage); + analysisError.onFile(inputFile).save(); + + verify(storage).store(analysisError); + verifyNoMoreInteractions(storage); + } + + @Test + public void test_no_storage() { + exception.expect(NullPointerException.class); + DefaultAnalysisError analysisError = new DefaultAnalysisError(); + analysisError.onFile(inputFile).save(); + } + + @Test + public void test_validation() { + try { + new DefaultAnalysisError(storage).onFile(null); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + // expected + } + + NewAnalysisError error = new DefaultAnalysisError(storage).onFile(inputFile); + try { + error.onFile(inputFile); + fail("Expected exception"); + } catch (IllegalStateException e) { + // expected + } + + error = new DefaultAnalysisError(storage).at(textPointer); + try { + error.at(textPointer); + fail("Expected exception"); + } catch (IllegalStateException e) { + // expected + } + + try { + new DefaultAnalysisError(storage).save(); + fail("Expected exception"); + } catch (NullPointerException e) { + // expected + } + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultCpdTokensTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultCpdTokensTest.java new file mode 100644 index 00000000000..fd2c60de2d1 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultCpdTokensTest.java @@ -0,0 +1,170 @@ +/* + * 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.impl.sensor; + +import org.junit.Test; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.impl.fs.DefaultInputFile; +import org.sonar.api.impl.fs.TestInputFileBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class DefaultCpdTokensTest { + private final SensorStorage sensorStorage = mock(SensorStorage.class); + + private final DefaultInputFile inputFile = new TestInputFileBuilder("foo", "src/Foo.java") + .setLines(2) + .setOriginalLineStartOffsets(new int[] {0, 50}) + .setOriginalLineEndOffsets(new int[] {49, 100}) + .setLastValidOffset(101) + .build(); + + @Test + public void save_no_tokens() { + DefaultCpdTokens tokens = new DefaultCpdTokens(sensorStorage) + .onFile(inputFile); + + tokens.save(); + + verify(sensorStorage).store(tokens); + + assertThat(tokens.inputFile()).isEqualTo(inputFile); + } + + @Test + public void save_one_token() { + DefaultCpdTokens tokens = new DefaultCpdTokens(sensorStorage) + .onFile(inputFile) + .addToken(inputFile.newRange(1, 2, 1, 5), "foo"); + + tokens.save(); + + verify(sensorStorage).store(tokens); + + assertThat(tokens.getTokenLines()).extracting("value", "startLine", "hashCode", "startUnit", "endUnit").containsExactly(tuple("foo", 1, "foo".hashCode(), 1, 1)); + } + + @Test + public void handle_exclusions() { + inputFile.setExcludedForDuplication(true); + DefaultCpdTokens tokens = new DefaultCpdTokens(sensorStorage) + .onFile(inputFile) + .addToken(inputFile.newRange(1, 2, 1, 5), "foo"); + + tokens.save(); + + verifyZeroInteractions(sensorStorage); + + assertThat(tokens.getTokenLines()).isEmpty(); + } + + @Test + public void dont_save_for_test_files() { + DefaultInputFile testInputFile = new TestInputFileBuilder("foo", "src/Foo.java") + .setLines(2) + .setOriginalLineStartOffsets(new int[] {0, 50}) + .setOriginalLineEndOffsets(new int[] {49, 100}) + .setLastValidOffset(101) + .setType(InputFile.Type.TEST) + .build(); + + DefaultCpdTokens tokens = new DefaultCpdTokens(sensorStorage) + .onFile(testInputFile) + .addToken(testInputFile.newRange(1, 2, 1, 5), "foo"); + + tokens.save(); + verifyZeroInteractions(sensorStorage); + assertThat(tokens.getTokenLines()).isEmpty(); + } + + @Test + public void save_many_tokens() { + DefaultCpdTokens tokens = new DefaultCpdTokens(sensorStorage) + .onFile(inputFile) + .addToken(inputFile.newRange(1, 2, 1, 5), "foo") + .addToken(inputFile.newRange(1, 6, 1, 10), "bar") + .addToken(inputFile.newRange(1, 20, 1, 25), "biz") + .addToken(inputFile.newRange(2, 1, 2, 10), "next"); + + tokens.save(); + + verify(sensorStorage).store(tokens); + + assertThat(tokens.getTokenLines()) + .extracting("value", "startLine", "hashCode", "startUnit", "endUnit") + .containsExactly( + tuple("foobarbiz", 1, "foobarbiz".hashCode(), 1, 3), + tuple("next", 2, "next".hashCode(), 4, 4)); + } + + @Test + public void basic_validation() { + SensorStorage sensorStorage = mock(SensorStorage.class); + DefaultCpdTokens tokens = new DefaultCpdTokens(sensorStorage); + try { + tokens.save(); + fail("Expected exception"); + } catch (Exception e) { + assertThat(e).hasMessage("Call onFile() first"); + } + try { + tokens.addToken(inputFile.newRange(1, 2, 1, 5), "foo"); + fail("Expected exception"); + } catch (Exception e) { + assertThat(e).hasMessage("Call onFile() first"); + } + try { + tokens.addToken(null, "foo"); + fail("Expected exception"); + } catch (Exception e) { + assertThat(e).hasMessage("Range should not be null"); + } + try { + tokens.addToken(inputFile.newRange(1, 2, 1, 5), null); + fail("Expected exception"); + } catch (Exception e) { + assertThat(e).hasMessage("Image should not be null"); + } + } + + @Test + public void validate_tokens_order() { + SensorStorage sensorStorage = mock(SensorStorage.class); + DefaultCpdTokens tokens = new DefaultCpdTokens(sensorStorage) + .onFile(inputFile) + .addToken(inputFile.newRange(1, 6, 1, 10), "bar"); + + try { + tokens.addToken(inputFile.newRange(1, 2, 1, 5), "foo"); + fail("Expected exception"); + } catch (Exception e) { + assertThat(e).hasMessage("Tokens of file src/Foo.java should be provided in order.\n" + + "Previous token: Range[from [line=1, lineOffset=6] to [line=1, lineOffset=10]]\n" + + "Last token: Range[from [line=1, lineOffset=2] to [line=1, lineOffset=5]]"); + } + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultExternalIssueTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultExternalIssueTest.java new file mode 100644 index 00000000000..06ff4064f88 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultExternalIssueTest.java @@ -0,0 +1,160 @@ +/* + * 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.impl.sensor; + +import java.io.IOException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.bootstrap.ProjectDefinition; +import org.sonar.api.batch.fs.InputComponent; +import org.sonar.api.batch.rule.Severity; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.RuleType; +import org.sonar.api.impl.fs.DefaultInputFile; +import org.sonar.api.impl.fs.DefaultInputProject; +import org.sonar.api.impl.fs.TestInputFileBuilder; +import org.sonar.api.impl.issue.DefaultIssueLocation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class DefaultExternalIssueTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private DefaultInputProject project; + + @Before + public void setup() throws IOException { + project = new DefaultInputProject(ProjectDefinition.create() + .setKey("foo") + .setBaseDir(temp.newFolder()) + .setWorkDir(temp.newFolder())); + } + + @Rule + public ExpectedException exception = ExpectedException.none(); + + private DefaultInputFile inputFile = new TestInputFileBuilder("foo", "src/Foo.php") + .initMetadata("Foo\nBar\n") + .build(); + + @Test + public void build_file_issue() { + SensorStorage storage = mock(SensorStorage.class); + DefaultExternalIssue issue = new DefaultExternalIssue(project, storage) + .at(new DefaultIssueLocation() + .on(inputFile) + .at(inputFile.selectLine(1)) + .message("Wrong way!")) + .forRule(RuleKey.of("repo", "rule")) + .remediationEffortMinutes(10l) + .type(RuleType.BUG) + .severity(Severity.BLOCKER); + + assertThat(issue.primaryLocation().inputComponent()).isEqualTo(inputFile); + assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("external_repo", "rule")); + assertThat(issue.engineId()).isEqualTo("repo"); + assertThat(issue.ruleId()).isEqualTo("rule"); + assertThat(issue.primaryLocation().textRange().start().line()).isEqualTo(1); + assertThat(issue.remediationEffort()).isEqualTo(10l); + assertThat(issue.type()).isEqualTo(RuleType.BUG); + assertThat(issue.severity()).isEqualTo(Severity.BLOCKER); + assertThat(issue.primaryLocation().message()).isEqualTo("Wrong way!"); + + issue.save(); + + verify(storage).store(issue); + } + + @Test + public void fail_to_store_if_no_type() { + SensorStorage storage = mock(SensorStorage.class); + DefaultExternalIssue issue = new DefaultExternalIssue(project, storage) + .at(new DefaultIssueLocation() + .on(inputFile) + .at(inputFile.selectLine(1)) + .message("Wrong way!")) + .forRule(RuleKey.of("repo", "rule")) + .remediationEffortMinutes(10l) + .severity(Severity.BLOCKER); + + exception.expect(IllegalStateException.class); + exception.expectMessage("Type is mandatory"); + issue.save(); + } + + @Test + public void fail_to_store_if_primary_location_is_not_a_file() { + SensorStorage storage = mock(SensorStorage.class); + DefaultExternalIssue issue = new DefaultExternalIssue(project, storage) + .at(new DefaultIssueLocation() + .on(mock(InputComponent.class)) + .message("Wrong way!")) + .forRule(RuleKey.of("repo", "rule")) + .remediationEffortMinutes(10l) + .severity(Severity.BLOCKER); + + exception.expect(IllegalStateException.class); + exception.expectMessage("External issues must be located in files"); + issue.save(); + } + + @Test + public void fail_to_store_if_primary_location_has_no_message() { + SensorStorage storage = mock(SensorStorage.class); + DefaultExternalIssue issue = new DefaultExternalIssue(project, storage) + .at(new DefaultIssueLocation() + .on(inputFile) + .at(inputFile.selectLine(1))) + .forRule(RuleKey.of("repo", "rule")) + .remediationEffortMinutes(10l) + .type(RuleType.BUG) + .severity(Severity.BLOCKER); + + exception.expect(IllegalStateException.class); + exception.expectMessage("External issues must have a message"); + issue.save(); + } + + @Test + public void fail_to_store_if_no_severity() { + SensorStorage storage = mock(SensorStorage.class); + DefaultExternalIssue issue = new DefaultExternalIssue(project, storage) + .at(new DefaultIssueLocation() + .on(inputFile) + .at(inputFile.selectLine(1)) + .message("Wrong way!")) + .forRule(RuleKey.of("repo", "rule")) + .remediationEffortMinutes(10l) + .type(RuleType.BUG); + + exception.expect(IllegalStateException.class); + exception.expectMessage("Severity is mandatory"); + issue.save(); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultHighlightingTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultHighlightingTest.java new file mode 100644 index 00000000000..99beb98927d --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultHighlightingTest.java @@ -0,0 +1,126 @@ +/* + * 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.impl.sensor; + +import java.util.Collection; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.impl.fs.DefaultTextPointer; +import org.sonar.api.impl.fs.DefaultTextRange; +import org.sonar.api.impl.fs.TestInputFileBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.sonar.api.batch.sensor.highlighting.TypeOfText.COMMENT; +import static org.sonar.api.batch.sensor.highlighting.TypeOfText.CPP_DOC; +import static org.sonar.api.batch.sensor.highlighting.TypeOfText.KEYWORD; + +public class DefaultHighlightingTest { + + private static final InputFile INPUT_FILE = new TestInputFileBuilder("foo", "src/Foo.java") + .setLines(2) + .setOriginalLineStartOffsets(new int[] {0, 50}) + .setOriginalLineEndOffsets(new int[] {49, 100}) + .setLastValidOffset(101) + .build(); + + private Collection<SyntaxHighlightingRule> highlightingRules; + + @Rule + public ExpectedException throwable = ExpectedException.none(); + + @Before + public void setUpSampleRules() { + + DefaultHighlighting highlightingDataBuilder = new DefaultHighlighting(mock(SensorStorage.class)) + .onFile(INPUT_FILE) + .highlight(0, 10, COMMENT) + .highlight(1, 10, 1, 12, KEYWORD) + .highlight(24, 38, KEYWORD) + .highlight(42, 50, KEYWORD) + .highlight(24, 65, CPP_DOC) + .highlight(12, 20, COMMENT); + + highlightingDataBuilder.save(); + + highlightingRules = highlightingDataBuilder.getSyntaxHighlightingRuleSet(); + } + + @Test + public void should_register_highlighting_rule() { + assertThat(highlightingRules).hasSize(6); + } + + private static TextRange rangeOf(int startLine, int startOffset, int endLine, int endOffset) { + return new DefaultTextRange(new DefaultTextPointer(startLine, startOffset), new DefaultTextPointer(endLine, endOffset)); + } + + @Test + public void should_order_by_start_then_end_offset() { + assertThat(highlightingRules).extracting("range", TextRange.class).containsExactly( + rangeOf(1, 0, 1, 10), + rangeOf(1, 10, 1, 12), + rangeOf(1, 12, 1, 20), + rangeOf(1, 24, 2, 15), + rangeOf(1, 24, 1, 38), + rangeOf(1, 42, 2, 0)); + assertThat(highlightingRules).extracting("textType").containsExactly(COMMENT, KEYWORD, COMMENT, CPP_DOC, KEYWORD, KEYWORD); + } + + @Test + public void should_support_overlapping() { + new DefaultHighlighting(mock(SensorStorage.class)) + .onFile(INPUT_FILE) + .highlight(0, 15, KEYWORD) + .highlight(8, 12, CPP_DOC) + .save(); + } + + @Test + public void should_prevent_start_equal_end() { + throwable.expect(IllegalArgumentException.class); + throwable + .expectMessage("Unable to highlight file"); + + new DefaultHighlighting(mock(SensorStorage.class)) + .onFile(INPUT_FILE) + .highlight(10, 10, KEYWORD) + .save(); + } + + @Test + public void should_prevent_boudaries_overlapping() { + throwable.expect(IllegalStateException.class); + throwable + .expectMessage("Cannot register highlighting rule for characters at Range[from [line=1, lineOffset=8] to [line=1, lineOffset=15]] as it overlaps at least one existing rule"); + + new DefaultHighlighting(mock(SensorStorage.class)) + .onFile(INPUT_FILE) + .highlight(0, 10, KEYWORD) + .highlight(8, 15, KEYWORD) + .save(); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultIssueLocationTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultIssueLocationTest.java new file mode 100644 index 00000000000..540a0bbc0c9 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultIssueLocationTest.java @@ -0,0 +1,108 @@ +/* + * 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.impl.sensor; + +import org.apache.commons.lang.StringUtils; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.impl.fs.TestInputFileBuilder; +import org.sonar.api.impl.issue.DefaultIssueLocation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.rules.ExpectedException.none; + +public class DefaultIssueLocationTest { + + @Rule + public ExpectedException thrown = none(); + + private InputFile inputFile = new TestInputFileBuilder("foo", "src/Foo.php") + .initMetadata("Foo\nBar\n") + .build(); + + @Test + public void should_build() { + assertThat(new DefaultIssueLocation() + .on(inputFile) + .message("pipo bimbo") + .message() + ).isEqualTo("pipo bimbo"); + } + + @Test + public void not_allowed_to_call_on_twice() { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("on() already called"); + new DefaultIssueLocation() + .on(inputFile) + .on(inputFile) + .message("Wrong way!"); + } + + @Test + public void prevent_too_long_messages() { + assertThat(new DefaultIssueLocation() + .on(inputFile) + .message(StringUtils.repeat("a", 4000)).message()).hasSize(4000); + + assertThat(new DefaultIssueLocation() + .on(inputFile) + .message(StringUtils.repeat("a", 4001)).message()).hasSize(4000); + } + + @Test + public void prevent_null_character_in_message_text() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Character \\u0000 is not supported in issue message"); + + new DefaultIssueLocation() + .message("pipo " + '\u0000' + " bimbo"); + } + + @Test + public void prevent_null_character_in_message_text_when_builder_has_been_initialized() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage(customMatcher("Character \\u0000 is not supported in issue message", ", on component: src/Foo.php")); + + new DefaultIssueLocation() + .on(inputFile) + .message("pipo " + '\u0000' + " bimbo"); + } + + private Matcher<String> customMatcher(String startWith, String endWith) { + return new TypeSafeMatcher<String>() { + @Override + public void describeTo(Description description) { + description.appendText("Invalid message"); + } + + @Override + protected boolean matchesSafely(final String item) { + return item.startsWith(startWith) && item.endsWith(endWith); + } + }; + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultIssueTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultIssueTest.java new file mode 100644 index 00000000000..f88a099f391 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultIssueTest.java @@ -0,0 +1,159 @@ +/* + * 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.impl.sensor; + +import java.io.File; +import java.io.IOException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.bootstrap.ProjectDefinition; +import org.sonar.api.batch.rule.Severity; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.impl.fs.DefaultInputDir; +import org.sonar.api.impl.fs.DefaultInputFile; +import org.sonar.api.impl.fs.DefaultInputModule; +import org.sonar.api.impl.fs.DefaultInputProject; +import org.sonar.api.impl.fs.TestInputFileBuilder; +import org.sonar.api.impl.issue.DefaultIssue; +import org.sonar.api.impl.issue.DefaultIssueLocation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class DefaultIssueTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private DefaultInputProject project; + + private DefaultInputFile inputFile = new TestInputFileBuilder("foo", "src/Foo.php") + .initMetadata("Foo\nBar\n") + .build(); + + @Before + public void prepare() throws IOException { + project = new DefaultInputProject(ProjectDefinition.create() + .setKey("foo") + .setBaseDir(temp.newFolder()) + .setWorkDir(temp.newFolder())); + } + + @Test + public void build_file_issue() { + SensorStorage storage = mock(SensorStorage.class); + DefaultIssue issue = new DefaultIssue(project, storage) + .at(new DefaultIssueLocation() + .on(inputFile) + .at(inputFile.selectLine(1)) + .message("Wrong way!")) + .forRule(RuleKey.of("repo", "rule")) + .gap(10.0); + + assertThat(issue.primaryLocation().inputComponent()).isEqualTo(inputFile); + assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("repo", "rule")); + assertThat(issue.primaryLocation().textRange().start().line()).isEqualTo(1); + assertThat(issue.gap()).isEqualTo(10.0); + assertThat(issue.primaryLocation().message()).isEqualTo("Wrong way!"); + + issue.save(); + + verify(storage).store(issue); + } + + @Test + public void move_directory_issue_to_project_root() { + SensorStorage storage = mock(SensorStorage.class); + DefaultIssue issue = new DefaultIssue(project, storage) + .at(new DefaultIssueLocation() + .on(new DefaultInputDir("foo", "src/main").setModuleBaseDir(project.getBaseDir())) + .message("Wrong way!")) + .forRule(RuleKey.of("repo", "rule")) + .overrideSeverity(Severity.BLOCKER); + + assertThat(issue.primaryLocation().inputComponent()).isEqualTo(project); + assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("repo", "rule")); + assertThat(issue.primaryLocation().textRange()).isNull(); + assertThat(issue.primaryLocation().message()).isEqualTo("[src/main] Wrong way!"); + assertThat(issue.overriddenSeverity()).isEqualTo(Severity.BLOCKER); + + issue.save(); + + verify(storage).store(issue); + } + + @Test + public void move_submodule_issue_to_project_root() { + File subModuleDirectory = new File(project.getBaseDir().toString(), "bar"); + subModuleDirectory.mkdir(); + + ProjectDefinition subModuleDefinition = ProjectDefinition.create() + .setKey("foo/bar") + .setBaseDir(subModuleDirectory) + .setWorkDir(subModuleDirectory); + project.definition().addSubProject(subModuleDefinition); + DefaultInputModule subModule = new DefaultInputModule(subModuleDefinition); + + SensorStorage storage = mock(SensorStorage.class); + DefaultIssue issue = new DefaultIssue(project, storage) + .at(new DefaultIssueLocation() + .on(subModule) + .message("Wrong way!")) + .forRule(RuleKey.of("repo", "rule")) + .overrideSeverity(Severity.BLOCKER); + + assertThat(issue.primaryLocation().inputComponent()).isEqualTo(project); + assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("repo", "rule")); + assertThat(issue.primaryLocation().textRange()).isNull(); + assertThat(issue.primaryLocation().message()).isEqualTo("[bar] Wrong way!"); + assertThat(issue.overriddenSeverity()).isEqualTo(Severity.BLOCKER); + + issue.save(); + + verify(storage).store(issue); + } + + @Test + public void build_project_issue() throws IOException { + SensorStorage storage = mock(SensorStorage.class); + DefaultInputModule inputModule = new DefaultInputModule(ProjectDefinition.create().setKey("foo").setBaseDir(temp.newFolder()).setWorkDir(temp.newFolder())); + DefaultIssue issue = new DefaultIssue(project, storage) + .at(new DefaultIssueLocation() + .on(inputModule) + .message("Wrong way!")) + .forRule(RuleKey.of("repo", "rule")) + .gap(10.0); + + assertThat(issue.primaryLocation().inputComponent()).isEqualTo(inputModule); + assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("repo", "rule")); + assertThat(issue.primaryLocation().textRange()).isNull(); + assertThat(issue.gap()).isEqualTo(10.0); + assertThat(issue.primaryLocation().message()).isEqualTo("Wrong way!"); + + issue.save(); + + verify(storage).store(issue); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultMeasureTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultMeasureTest.java new file mode 100644 index 00000000000..0c7ece260ce --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultMeasureTest.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.api.impl.sensor; + +import java.io.IOException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.bootstrap.ProjectDefinition; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.impl.fs.AbstractProjectOrModule; +import org.sonar.api.impl.fs.DefaultInputProject; +import org.sonar.api.impl.fs.TestInputFileBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class DefaultMeasureTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void build_file_measure() { + SensorStorage storage = mock(SensorStorage.class); + DefaultMeasure<Integer> newMeasure = new DefaultMeasure<Integer>(storage) + .forMetric(CoreMetrics.LINES) + .on(new TestInputFileBuilder("foo", "src/Foo.php").build()) + .withValue(3); + + assertThat(newMeasure.inputComponent()).isEqualTo(new TestInputFileBuilder("foo", "src/Foo.php").build()); + assertThat(newMeasure.metric()).isEqualTo(CoreMetrics.LINES); + assertThat(newMeasure.value()).isEqualTo(3); + + newMeasure.save(); + + verify(storage).store(newMeasure); + } + + @Test + public void build_project_measure() throws IOException { + SensorStorage storage = mock(SensorStorage.class); + AbstractProjectOrModule module = new DefaultInputProject(ProjectDefinition.create().setKey("foo").setBaseDir(temp.newFolder()).setWorkDir(temp.newFolder())); + DefaultMeasure<Integer> newMeasure = new DefaultMeasure<Integer>(storage) + .forMetric(CoreMetrics.LINES) + .on(module) + .withValue(3); + + assertThat(newMeasure.inputComponent()).isEqualTo(module); + assertThat(newMeasure.metric()).isEqualTo(CoreMetrics.LINES); + assertThat(newMeasure.value()).isEqualTo(3); + + newMeasure.save(); + + verify(storage).store(newMeasure); + } + + @Test + public void not_allowed_to_call_on_twice() throws IOException { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("on() already called"); + new DefaultMeasure<Integer>() + .on(new DefaultInputProject(ProjectDefinition.create().setKey("foo").setBaseDir(temp.newFolder()).setWorkDir(temp.newFolder()))) + .on(new TestInputFileBuilder("foo", "src/Foo.php").build()) + .withValue(3) + .save(); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultSensorDescriptorTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultSensorDescriptorTest.java new file mode 100644 index 00000000000..b09b5ed9eab --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultSensorDescriptorTest.java @@ -0,0 +1,51 @@ +/* + * 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.impl.sensor; + +import org.junit.Test; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.impl.config.MapSettings; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DefaultSensorDescriptorTest { + + @Test + public void describe() { + DefaultSensorDescriptor descriptor = new DefaultSensorDescriptor(); + descriptor + .name("Foo") + .onlyOnLanguage("java") + .onlyOnFileType(InputFile.Type.MAIN) + .requireProperty("sonar.foo.reportPath", "sonar.foo.reportPath2") + .createIssuesForRuleRepository("squid-java"); + + assertThat(descriptor.name()).isEqualTo("Foo"); + assertThat(descriptor.languages()).containsOnly("java"); + assertThat(descriptor.type()).isEqualTo(InputFile.Type.MAIN); + MapSettings settings = new MapSettings(); + settings.setProperty("sonar.foo.reportPath", "foo"); + assertThat(descriptor.configurationPredicate().test(settings.asConfig())).isFalse(); + settings.setProperty("sonar.foo.reportPath2", "foo"); + assertThat(descriptor.configurationPredicate().test(settings.asConfig())).isTrue(); + assertThat(descriptor.ruleRepositories()).containsOnly("squid-java"); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultSignificantCodeTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultSignificantCodeTest.java new file mode 100644 index 00000000000..f6d7f45281e --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultSignificantCodeTest.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.api.impl.sensor; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.impl.fs.TestInputFileBuilder; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class DefaultSignificantCodeTest { + private SensorStorage sensorStorage = mock(SensorStorage.class); + private DefaultSignificantCode underTest = new DefaultSignificantCode(sensorStorage); + private InputFile inputFile = TestInputFileBuilder.create("module", "file1.xoo") + .setContents("this is\na file\n with some code") + .build(); + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void should_save_ranges() { + underTest.onFile(inputFile) + .addRange(inputFile.selectLine(1)) + .save(); + verify(sensorStorage).store(underTest); + } + + @Test + public void fail_if_save_without_file() { + exception.expect(IllegalStateException.class); + exception.expectMessage("Call onFile() first"); + underTest.save(); + } + + @Test + public void fail_if_add_range_to_same_line_twice() { + underTest.onFile(inputFile); + underTest.addRange(inputFile.selectLine(1)); + + exception.expect(IllegalStateException.class); + exception.expectMessage("Significant code was already reported for line '1'."); + underTest.addRange(inputFile.selectLine(1)); + } + + @Test + public void fail_if_range_includes_many_lines() { + underTest.onFile(inputFile); + + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Ranges of significant code must be located in a single line"); + underTest.addRange(inputFile.newRange(1, 1, 2, 1)); + } + + @Test + public void fail_if_add_range_before_setting_file() { + exception.expect(IllegalStateException.class); + exception.expectMessage("addRange() should be called after on()"); + underTest.addRange(inputFile.selectLine(1)); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultSymbolTableTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultSymbolTableTest.java new file mode 100644 index 00000000000..affd87bbef4 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/DefaultSymbolTableTest.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.api.impl.sensor; + +import java.util.Map; +import java.util.Set; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.batch.sensor.internal.SensorStorage; +import org.sonar.api.impl.fs.TestInputFileBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class DefaultSymbolTableTest { + + private static final InputFile INPUT_FILE = new TestInputFileBuilder("foo", "src/Foo.java") + .setLines(2) + .setOriginalLineStartOffsets(new int[] {0, 50}) + .setOriginalLineEndOffsets(new int[] {49, 100}) + .setLastValidOffset(101) + .build(); + + private Map<TextRange, Set<TextRange>> referencesPerSymbol; + + @Rule + public ExpectedException throwable = ExpectedException.none(); + + @Before + public void setUpSampleSymbols() { + + DefaultSymbolTable symbolTableBuilder = new DefaultSymbolTable(mock(SensorStorage.class)) + .onFile(INPUT_FILE); + symbolTableBuilder + .newSymbol(0, 10) + .newReference(12, 15) + .newReference(2, 10, 2, 15); + + symbolTableBuilder.newSymbol(1, 12, 1, 15).newReference(52, 55); + + symbolTableBuilder.save(); + + referencesPerSymbol = symbolTableBuilder.getReferencesBySymbol(); + } + + @Test + public void should_register_symbols() { + assertThat(referencesPerSymbol).hasSize(2); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/InMemorySensorStorageTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/InMemorySensorStorageTest.java new file mode 100644 index 00000000000..25b44f5cfbb --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/InMemorySensorStorageTest.java @@ -0,0 +1,60 @@ +/* + * 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.impl.sensor; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.impl.sensor.InMemorySensorStorage; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; + +public class InMemorySensorStorageTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + InMemorySensorStorage underTest = new InMemorySensorStorage(); + + @Test + public void test_storeProperty() { + assertThat(underTest.contextProperties).isEmpty(); + + underTest.storeProperty("foo", "bar"); + assertThat(underTest.contextProperties).containsOnly(entry("foo", "bar")); + } + + @Test + public void storeProperty_throws_IAE_if_key_is_null() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Key of context property must not be null"); + + underTest.storeProperty(null, "bar"); + } + + @Test + public void storeProperty_throws_IAE_if_value_is_null() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Value of context property must not be null"); + + underTest.storeProperty("foo", null); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/SensorContextTesterTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/SensorContextTesterTest.java new file mode 100644 index 00000000000..9684c7cdcb2 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/sensor/SensorContextTesterTest.java @@ -0,0 +1,375 @@ +/* + * 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.impl.sensor; + +import java.io.File; +import java.io.IOException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.bootstrap.ProjectDefinition; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.rule.ActiveRules; +import org.sonar.api.impl.rule.ActiveRulesBuilder; +import org.sonar.api.batch.rule.NewActiveRule; +import org.sonar.api.batch.rule.Severity; +import org.sonar.api.batch.sensor.error.AnalysisError; +import org.sonar.api.batch.sensor.error.NewAnalysisError; +import org.sonar.api.batch.sensor.highlighting.TypeOfText; +import org.sonar.api.batch.sensor.issue.NewExternalIssue; +import org.sonar.api.batch.sensor.issue.NewIssue; +import org.sonar.api.batch.sensor.symbol.NewSymbolTable; +import org.sonar.api.config.Settings; +import org.sonar.api.impl.config.MapSettings; +import org.sonar.api.impl.fs.DefaultFileSystem; +import org.sonar.api.impl.fs.DefaultInputFile; +import org.sonar.api.impl.fs.DefaultInputModule; +import org.sonar.api.impl.fs.DefaultTextPointer; +import org.sonar.api.impl.fs.TestInputFileBuilder; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.RuleType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.assertj.core.data.MapEntry.entry; + +public class SensorContextTesterTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public ExpectedException exception = ExpectedException.none(); + + private SensorContextTester tester; + private File baseDir; + + @Before + public void prepare() throws Exception { + baseDir = temp.newFolder(); + tester = SensorContextTester.create(baseDir); + } + + @Test + public void testSettings() { + Settings settings = new MapSettings(); + settings.setProperty("foo", "bar"); + tester.setSettings(settings); + assertThat(tester.settings().getString("foo")).isEqualTo("bar"); + } + + @Test + public void testActiveRules() { + NewActiveRule activeRule = new NewActiveRule.Builder() + .setRuleKey(RuleKey.of("foo", "bar")) + .build(); + ActiveRules activeRules = new ActiveRulesBuilder().addRule(activeRule).build(); + tester.setActiveRules(activeRules); + assertThat(tester.activeRules().findAll()).hasSize(1); + } + + @Test + public void testFs() throws Exception { + DefaultFileSystem fs = new DefaultFileSystem(temp.newFolder()); + tester.setFileSystem(fs); + assertThat(tester.fileSystem().baseDir()).isNotEqualTo(baseDir); + } + + @Test + public void testIssues() { + assertThat(tester.allIssues()).isEmpty(); + NewIssue newIssue = tester.newIssue(); + newIssue + .at(newIssue.newLocation().on(new TestInputFileBuilder("foo", "src/Foo.java").build())) + .forRule(RuleKey.of("repo", "rule")) + .save(); + newIssue = tester.newIssue(); + newIssue + .at(newIssue.newLocation().on(new TestInputFileBuilder("foo", "src/Foo.java").build())) + .forRule(RuleKey.of("repo", "rule")) + .save(); + assertThat(tester.allIssues()).hasSize(2); + } + + @Test + public void testExternalIssues() { + assertThat(tester.allExternalIssues()).isEmpty(); + NewExternalIssue newExternalIssue = tester.newExternalIssue(); + newExternalIssue + .at(newExternalIssue.newLocation().message("message").on(new TestInputFileBuilder("foo", "src/Foo.java").build())) + .forRule(RuleKey.of("repo", "rule")) + .type(RuleType.BUG) + .severity(Severity.BLOCKER) + .save(); + newExternalIssue = tester.newExternalIssue(); + newExternalIssue + .at(newExternalIssue.newLocation().message("message").on(new TestInputFileBuilder("foo", "src/Foo.java").build())) + .type(RuleType.BUG) + .severity(Severity.BLOCKER) + .forRule(RuleKey.of("repo", "rule")) + .save(); + assertThat(tester.allExternalIssues()).hasSize(2); + } + + @Test + public void testAnalysisErrors() { + assertThat(tester.allAnalysisErrors()).isEmpty(); + NewAnalysisError newAnalysisError = tester.newAnalysisError(); + + InputFile file = new TestInputFileBuilder("foo", "src/Foo.java").build(); + newAnalysisError.onFile(file) + .message("error") + .at(new DefaultTextPointer(5, 2)) + .save(); + + assertThat(tester.allAnalysisErrors()).hasSize(1); + AnalysisError analysisError = tester.allAnalysisErrors().iterator().next(); + + assertThat(analysisError.inputFile()).isEqualTo(file); + assertThat(analysisError.message()).isEqualTo("error"); + assertThat(analysisError.location()).isEqualTo(new DefaultTextPointer(5, 2)); + + } + + @Test + public void testMeasures() throws IOException { + assertThat(tester.measures("foo:src/Foo.java")).isEmpty(); + assertThat(tester.measure("foo:src/Foo.java", "ncloc")).isNull(); + tester.<Integer>newMeasure() + .on(new TestInputFileBuilder("foo", "src/Foo.java").build()) + .forMetric(CoreMetrics.NCLOC) + .withValue(2) + .save(); + assertThat(tester.measures("foo:src/Foo.java")).hasSize(1); + assertThat(tester.measure("foo:src/Foo.java", "ncloc")).isNotNull(); + tester.<Integer>newMeasure() + .on(new TestInputFileBuilder("foo", "src/Foo.java").build()) + .forMetric(CoreMetrics.LINES) + .withValue(4) + .save(); + assertThat(tester.measures("foo:src/Foo.java")).hasSize(2); + assertThat(tester.measure("foo:src/Foo.java", "ncloc")).isNotNull(); + assertThat(tester.measure("foo:src/Foo.java", "lines")).isNotNull(); + tester.<Integer>newMeasure() + .on(new DefaultInputModule(ProjectDefinition.create().setKey("foo").setBaseDir(temp.newFolder()).setWorkDir(temp.newFolder()))) + .forMetric(CoreMetrics.DIRECTORIES) + .withValue(4) + .save(); + assertThat(tester.measures("foo")).hasSize(1); + assertThat(tester.measure("foo", "directories")).isNotNull(); + } + + @Test(expected = IllegalStateException.class) + public void duplicateMeasures() { + tester.<Integer>newMeasure() + .on(new TestInputFileBuilder("foo", "src/Foo.java").build()) + .forMetric(CoreMetrics.NCLOC) + .withValue(2) + .save(); + tester.<Integer>newMeasure() + .on(new TestInputFileBuilder("foo", "src/Foo.java").build()) + .forMetric(CoreMetrics.NCLOC) + .withValue(2) + .save(); + } + + @Test + public void testHighlighting() { + assertThat(tester.highlightingTypeAt("foo:src/Foo.java", 1, 3)).isEmpty(); + tester.newHighlighting() + .onFile(new TestInputFileBuilder("foo", "src/Foo.java").initMetadata("annot dsf fds foo bar").build()) + .highlight(1, 0, 1, 5, TypeOfText.ANNOTATION) + .highlight(8, 10, TypeOfText.CONSTANT) + .highlight(9, 10, TypeOfText.COMMENT) + .save(); + assertThat(tester.highlightingTypeAt("foo:src/Foo.java", 1, 3)).containsExactly(TypeOfText.ANNOTATION); + assertThat(tester.highlightingTypeAt("foo:src/Foo.java", 1, 9)).containsExactly(TypeOfText.CONSTANT, TypeOfText.COMMENT); + } + + @Test(expected = UnsupportedOperationException.class) + public void duplicateHighlighting() { + tester.newHighlighting() + .onFile(new TestInputFileBuilder("foo", "src/Foo.java").initMetadata("annot dsf fds foo bar").build()) + .highlight(1, 0, 1, 5, TypeOfText.ANNOTATION) + .save(); + tester.newHighlighting() + .onFile(new TestInputFileBuilder("foo", "src/Foo.java").initMetadata("annot dsf fds foo bar").build()) + .highlight(1, 0, 1, 5, TypeOfText.ANNOTATION) + .save(); + } + + @Test + public void testSymbolReferences() { + assertThat(tester.referencesForSymbolAt("foo:src/Foo.java", 1, 0)).isNull(); + + NewSymbolTable symbolTable = tester.newSymbolTable() + .onFile(new TestInputFileBuilder("foo", "src/Foo.java").initMetadata("annot dsf fds foo bar").build()); + symbolTable + .newSymbol(1, 8, 1, 10); + + symbolTable + .newSymbol(1, 1, 1, 5) + .newReference(6, 9) + .newReference(1, 10, 1, 13); + + symbolTable.save(); + + assertThat(tester.referencesForSymbolAt("foo:src/Foo.java", 1, 0)).isNull(); + assertThat(tester.referencesForSymbolAt("foo:src/Foo.java", 1, 8)).isEmpty(); + assertThat(tester.referencesForSymbolAt("foo:src/Foo.java", 1, 3)).extracting("start.line", "start.lineOffset", "end.line", "end.lineOffset").containsExactly(tuple(1, 6, 1, 9), + tuple(1, 10, 1, 13)); + } + + @Test(expected = UnsupportedOperationException.class) + public void duplicateSymbolReferences() { + NewSymbolTable symbolTable = tester.newSymbolTable() + .onFile(new TestInputFileBuilder("foo", "src/Foo.java").initMetadata("annot dsf fds foo bar").build()); + symbolTable + .newSymbol(1, 8, 1, 10); + + symbolTable.save(); + + symbolTable = tester.newSymbolTable() + .onFile(new TestInputFileBuilder("foo", "src/Foo.java").initMetadata("annot dsf fds foo bar").build()); + symbolTable + .newSymbol(1, 8, 1, 10); + + symbolTable.save(); + } + + @Test + public void testCoverageAtLineZero() { + assertThat(tester.lineHits("foo:src/Foo.java", 1)).isNull(); + assertThat(tester.lineHits("foo:src/Foo.java", 4)).isNull(); + + exception.expect(IllegalStateException.class); + tester.newCoverage() + .onFile(new TestInputFileBuilder("foo", "src/Foo.java").initMetadata("annot dsf fds foo bar").build()) + .lineHits(0, 3); + } + + @Test + public void testCoverageAtLineOutOfRange() { + assertThat(tester.lineHits("foo:src/Foo.java", 1)).isNull(); + assertThat(tester.lineHits("foo:src/Foo.java", 4)).isNull(); + exception.expect(IllegalStateException.class); + + tester.newCoverage() + .onFile(new TestInputFileBuilder("foo", "src/Foo.java").initMetadata("annot dsf fds foo bar").build()) + .lineHits(4, 3); + } + + @Test + public void testLineHits() { + assertThat(tester.lineHits("foo:src/Foo.java", 1)).isNull(); + assertThat(tester.lineHits("foo:src/Foo.java", 4)).isNull(); + tester.newCoverage() + .onFile(new TestInputFileBuilder("foo", "src/Foo.java").initMetadata("annot dsf fds foo bar\nasdas").build()) + .lineHits(1, 2) + .lineHits(2, 3) + .save(); + assertThat(tester.lineHits("foo:src/Foo.java", 1)).isEqualTo(2); + assertThat(tester.lineHits("foo:src/Foo.java", 2)).isEqualTo(3); + } + + public void multipleCoverage() { + tester.newCoverage() + .onFile(new TestInputFileBuilder("foo", "src/Foo.java").initMetadata("annot dsf fds foo bar\nasdas").build()) + .lineHits(1, 2) + .conditions(3, 4, 2) + .save(); + tester.newCoverage() + .onFile(new TestInputFileBuilder("foo", "src/Foo.java").initMetadata("annot dsf fds foo bar\nasdas").build()) + .lineHits(1, 2) + .conditions(3, 4, 3) + .save(); + assertThat(tester.lineHits("foo:src/Foo.java", 1)).isEqualTo(4); + assertThat(tester.conditions("foo:src/Foo.java", 3)).isEqualTo(4); + assertThat(tester.coveredConditions("foo:src/Foo.java", 3)).isEqualTo(3); + } + + @Test + public void testConditions() { + assertThat(tester.conditions("foo:src/Foo.java", 1)).isNull(); + assertThat(tester.coveredConditions("foo:src/Foo.java", 1)).isNull(); + tester.newCoverage() + .onFile(new TestInputFileBuilder("foo", "src/Foo.java") + .initMetadata("annot dsf fds foo bar\nasd\nasdas\nasdfas") + .build()) + .conditions(1, 4, 2) + .save(); + assertThat(tester.conditions("foo:src/Foo.java", 1)).isEqualTo(4); + assertThat(tester.coveredConditions("foo:src/Foo.java", 1)).isEqualTo(2); + } + + @Test + public void testCpdTokens() { + assertThat(tester.cpdTokens("foo:src/Foo.java")).isNull(); + DefaultInputFile inputFile = new TestInputFileBuilder("foo", "src/Foo.java") + .initMetadata("public class Foo {\n\n}") + .build(); + tester.newCpdTokens() + .onFile(inputFile) + .addToken(inputFile.newRange(0, 6), "public") + .addToken(inputFile.newRange(7, 12), "class") + .addToken(inputFile.newRange(13, 16), "$IDENTIFIER") + .addToken(inputFile.newRange(17, 18), "{") + .addToken(inputFile.newRange(3, 0, 3, 1), "}") + .save(); + assertThat(tester.cpdTokens("foo:src/Foo.java")).extracting("value", "startLine", "startUnit", "endUnit") + .containsExactly( + tuple("publicclass$IDENTIFIER{", 1, 1, 4), + tuple("}", 3, 5, 5)); + } + + @Test(expected = UnsupportedOperationException.class) + public void duplicateCpdTokens() { + DefaultInputFile inputFile = new TestInputFileBuilder("foo", "src/Foo.java") + .initMetadata("public class Foo {\n\n}") + .build(); + tester.newCpdTokens() + .onFile(inputFile) + .addToken(inputFile.newRange(0, 6), "public") + .save(); + + tester.newCpdTokens() + .onFile(inputFile) + .addToken(inputFile.newRange(0, 6), "public") + .save(); + } + + @Test + public void testCancellation() { + assertThat(tester.isCancelled()).isFalse(); + tester.setCancelled(true); + assertThat(tester.isCancelled()).isTrue(); + } + + @Test + public void testContextProperties() { + assertThat(tester.getContextProperties()).isEmpty(); + + tester.addContextProperty("foo", "bar"); + assertThat(tester.getContextProperties()).containsOnly(entry("foo", "bar")); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/server/DefaultNewRuleTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/server/DefaultNewRuleTest.java new file mode 100644 index 00000000000..361785b49f7 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/server/DefaultNewRuleTest.java @@ -0,0 +1,98 @@ +/* + * 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.impl.server; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rule.RuleScope; +import org.sonar.api.rule.RuleStatus; +import org.sonar.api.server.rule.RulesDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DefaultNewRuleTest { + @Rule + public ExpectedException exception = ExpectedException.none(); + + private DefaultNewRule rule = new DefaultNewRule("plugin", "repo", "key"); + + @Test + public void testSimpleSetGet() { + assertThat(rule.pluginKey()).isEqualTo("plugin"); + assertThat(rule.repoKey()).isEqualTo("repo"); + assertThat(rule.key()).isEqualTo("key"); + + rule.setScope(RuleScope.MAIN); + assertThat(rule.scope()).isEqualTo(RuleScope.MAIN); + + rule.setName(" name "); + assertThat(rule.name()).isEqualTo("name"); + + rule.setHtmlDescription(" html "); + assertThat(rule.htmlDescription()).isEqualTo("html"); + + rule.setTemplate(true); + assertThat(rule.template()).isTrue(); + + rule.setActivatedByDefault(true); + assertThat(rule.activatedByDefault()).isTrue(); + + RulesDefinition.NewParam param1 = rule.createParam("param1"); + assertThat(rule.param("param1")).isEqualTo(param1); + assertThat(rule.params()).containsOnly(param1); + + rule.setTags("tag1", "tag2"); + rule.addTags("tag3"); + assertThat(rule.tags()).containsExactly("tag1", "tag2", "tag3"); + + rule.setEffortToFixDescription("effort"); + assertThat(rule.gapDescription()).isEqualTo("effort"); + + rule.setGapDescription("gap"); + assertThat(rule.gapDescription()).isEqualTo("gap"); + + rule.setInternalKey("internal"); + assertThat(rule.internalKey()).isEqualTo("internal"); + + rule.addDeprecatedRuleKey("deprecatedrepo", "deprecatedkey"); + assertThat(rule.deprecatedRuleKeys()).containsOnly(RuleKey.of("deprecatedrepo", "deprecatedkey")); + } + + @Test + public void fail_if_severity_is_invalid() { + exception.expect(IllegalArgumentException.class); + rule.setSeverity("invalid"); + } + + @Test + public void fail_setting_markdown_if_html_is_set() { + exception.expect(IllegalStateException.class); + rule.setHtmlDescription("html"); + rule.setMarkdownDescription("markdown"); + } + + @Test + public void fail_if_set_status_to_removed() { + exception.expect(IllegalArgumentException.class); + rule.setStatus(RuleStatus.REMOVED); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/utils/AlwaysIncreasingSystem2Test.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/utils/AlwaysIncreasingSystem2Test.java new file mode 100644 index 00000000000..30953e690d6 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/utils/AlwaysIncreasingSystem2Test.java @@ -0,0 +1,118 @@ +/* + * 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.impl.utils; + +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AlwaysIncreasingSystem2Test { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void default_constructor_makes_now_start_with_random_number_and_increase_returned_value_by_100_with_each_call() { + AlwaysIncreasingSystem2 underTest = new AlwaysIncreasingSystem2(); + verifyValuesReturnedByNow(underTest, null, 100); + } + + @Test + public void constructor_with_increment_makes_now_start_with_random_number_and_increase_returned_value_by_specified_value_with_each_call() { + AlwaysIncreasingSystem2 underTest = new AlwaysIncreasingSystem2(663); + + verifyValuesReturnedByNow(underTest, null, 663); + } + + @Test + public void constructor_with_initial_value_and_increment_makes_now_start_with_specified_value_and_increase_returned_value_by_specified_value_with_each_call() { + AlwaysIncreasingSystem2 underTest = new AlwaysIncreasingSystem2(777777L, 96); + + verifyValuesReturnedByNow(underTest, 777777L, 96); + } + + @Test + public void constructor_with_initial_value_and_increment_throws_IAE_if_initial_value_is_less_than_0() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Initial value must be >= 0"); + + new AlwaysIncreasingSystem2(-1, 100); + } + + @Test + public void constructor_with_initial_value_and_increment_accepts_initial_value_0() { + AlwaysIncreasingSystem2 underTest = new AlwaysIncreasingSystem2(0, 100); + + verifyValuesReturnedByNow(underTest, 0L, 100); + } + + @Test + public void constructor_with_initial_value_and_increment_throws_IAE_if_increment_is_0() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("increment must be > 0"); + + new AlwaysIncreasingSystem2(10, 0); + } + + @Test + public void constructor_with_initial_value_and_increment_throws_IAE_if_increment_is_less_than_0() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("increment must be > 0"); + + new AlwaysIncreasingSystem2(10, -66); + } + + @Test + public void constructor_with_increment_throws_IAE_if_increment_is_0() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("increment must be > 0"); + + new AlwaysIncreasingSystem2(0); + } + + @Test + public void constructor_with_increment_throws_IAE_if_increment_is_less_than_0() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("increment must be > 0"); + + new AlwaysIncreasingSystem2(-20); + } + + private void verifyValuesReturnedByNow(AlwaysIncreasingSystem2 underTest, @Nullable Long initialValue, int increment) { + long previousValue = -1; + for (int i = 0; i < 333; i++) { + if (previousValue == -1) { + long now = underTest.now(); + if (initialValue != null) { + assertThat(now).isEqualTo(initialValue); + } else { + assertThat(now).isGreaterThan(0); + } + previousValue = now; + } else { + long now = underTest.now(); + assertThat(now).isEqualTo(previousValue + increment); + previousValue = now; + } + } + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/utils/DefaultTempFolderTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/utils/DefaultTempFolderTest.java new file mode 100644 index 00000000000..b21d2077ebb --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/utils/DefaultTempFolderTest.java @@ -0,0 +1,124 @@ +/* + * 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.impl.utils; + +import java.io.File; +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.server.util.TempFolderCleaner; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DefaultTempFolderTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public LogTester logTester = new LogTester(); + + @Test + public void createTempFolderAndFile() throws Exception { + File rootTempFolder = temp.newFolder(); + DefaultTempFolder underTest = new DefaultTempFolder(rootTempFolder); + File dir = underTest.newDir(); + assertThat(dir).exists().isDirectory(); + File file = underTest.newFile(); + assertThat(file).exists().isFile(); + + new TempFolderCleaner(underTest).stop(); + assertThat(rootTempFolder).doesNotExist(); + } + + @Test + public void createTempFolderWithName() throws Exception { + File rootTempFolder = temp.newFolder(); + DefaultTempFolder underTest = new DefaultTempFolder(rootTempFolder); + File dir = underTest.newDir("sample"); + assertThat(dir).exists().isDirectory(); + assertThat(new File(rootTempFolder, "sample")).isEqualTo(dir); + + new TempFolderCleaner(underTest).stop(); + assertThat(rootTempFolder).doesNotExist(); + } + + @Test + public void newDir_throws_ISE_if_name_is_not_valid() throws Exception { + File rootTempFolder = temp.newFolder(); + DefaultTempFolder underTest = new DefaultTempFolder(rootTempFolder); + String tooLong = "tooooolong"; + for (int i = 0; i < 50; i++) { + tooLong += "tooooolong"; + } + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Failed to create temp directory"); + + underTest.newDir(tooLong); + } + + @Test + public void newFile_throws_ISE_if_name_is_not_valid() throws Exception { + File rootTempFolder = temp.newFolder(); + DefaultTempFolder underTest = new DefaultTempFolder(rootTempFolder); + String tooLong = "tooooolong"; + for (int i = 0; i < 50; i++) { + tooLong += "tooooolong"; + } + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Failed to create temp file"); + + underTest.newFile(tooLong, ".txt"); + } + + @Test + public void clean_deletes_non_empty_directory() throws Exception { + File dir = temp.newFolder(); + FileUtils.touch(new File(dir, "foo.txt")); + + DefaultTempFolder underTest = new DefaultTempFolder(dir); + underTest.clean(); + + assertThat(dir).doesNotExist(); + } + + @Test + public void clean_does_not_fail_if_directory_has_already_been_deleted() throws Exception { + File dir = temp.newFolder(); + + DefaultTempFolder underTest = new DefaultTempFolder(dir); + underTest.clean(); + assertThat(dir).doesNotExist(); + + // second call does not fail, nor log ERROR logs + underTest.clean(); + + assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty(); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/utils/JUnitTempFolderTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/utils/JUnitTempFolderTest.java new file mode 100644 index 00000000000..5b765c344cc --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/utils/JUnitTempFolderTest.java @@ -0,0 +1,53 @@ +/* + * 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.impl.utils; + +import org.junit.Test; + +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JUnitTempFolderTest { + + @Test + public void apply() throws Throwable { + JUnitTempFolder temp = new JUnitTempFolder(); + temp.before(); + File dir1 = temp.newDir(); + assertThat(dir1).isDirectory().exists(); + + File dir2 = temp.newDir("foo"); + assertThat(dir2).isDirectory().exists(); + + File file1 = temp.newFile(); + assertThat(file1).isFile().exists(); + + File file2 = temp.newFile("foo", "txt"); + assertThat(file2).isFile().exists(); + + temp.after(); + assertThat(dir1).doesNotExist(); + assertThat(dir2).doesNotExist(); + assertThat(file1).doesNotExist(); + assertThat(file2).doesNotExist(); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/utils/ScannerUtilsTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/utils/ScannerUtilsTest.java new file mode 100644 index 00000000000..0390d1faf90 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/utils/ScannerUtilsTest.java @@ -0,0 +1,52 @@ +/* + * 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.impl.utils; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ScannerUtilsTest { + @Test + public void test_pluralize() { + assertThat(ScannerUtils.pluralize("string", 0)).isEqualTo("strings"); + assertThat(ScannerUtils.pluralize("string", 1)).isEqualTo("string"); + assertThat(ScannerUtils.pluralize("string", 2)).isEqualTo("strings"); + } + + @Test + public void cleanKeyForFilename() { + assertThat(ScannerUtils.cleanKeyForFilename("project 1")).isEqualTo("project1"); + assertThat(ScannerUtils.cleanKeyForFilename("project:1")).isEqualTo("project_1"); + } + + @Test + public void describe() { + assertThat(ScannerUtils.describe(new Object())).isEqualTo("java.lang.Object"); + assertThat(ScannerUtils.describe(new TestClass())).isEqualTo("overridden"); + } + + class TestClass { + @Override + public String toString() { + return "overridden"; + } + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/utils/WorkDurationTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/utils/WorkDurationTest.java new file mode 100644 index 00000000000..3c1e2f971ff --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/utils/WorkDurationTest.java @@ -0,0 +1,200 @@ +/* + * 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.impl.utils; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class WorkDurationTest { + + static final int HOURS_IN_DAY = 8; + + static final Long ONE_MINUTE = 1L; + static final Long ONE_HOUR_IN_MINUTES = ONE_MINUTE * 60; + static final Long ONE_DAY_IN_MINUTES = ONE_HOUR_IN_MINUTES * HOURS_IN_DAY; + + @Test + public void create_from_days_hours_minutes() { + WorkDuration workDuration = WorkDuration.create(1, 1, 1, HOURS_IN_DAY); + assertThat(workDuration.days()).isEqualTo(1); + assertThat(workDuration.hours()).isEqualTo(1); + assertThat(workDuration.minutes()).isEqualTo(1); + assertThat(workDuration.toMinutes()).isEqualTo(ONE_DAY_IN_MINUTES + ONE_HOUR_IN_MINUTES + ONE_MINUTE); + assertThat(workDuration.hoursInDay()).isEqualTo(HOURS_IN_DAY); + } + + @Test + public void create_from_value_and_unit() { + WorkDuration result = WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.DAYS, HOURS_IN_DAY); + assertThat(result.days()).isEqualTo(1); + assertThat(result.hours()).isEqualTo(0); + assertThat(result.minutes()).isEqualTo(0); + assertThat(result.hoursInDay()).isEqualTo(HOURS_IN_DAY); + assertThat(result.toMinutes()).isEqualTo(ONE_DAY_IN_MINUTES); + + assertThat(WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.DAYS, HOURS_IN_DAY).toMinutes()).isEqualTo(ONE_DAY_IN_MINUTES); + assertThat(WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toMinutes()).isEqualTo(ONE_HOUR_IN_MINUTES); + assertThat(WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).toMinutes()).isEqualTo(ONE_MINUTE); + } + + @Test + public void create_from_minutes() { + WorkDuration workDuration = WorkDuration.createFromMinutes(ONE_MINUTE, HOURS_IN_DAY); + assertThat(workDuration.days()).isEqualTo(0); + assertThat(workDuration.hours()).isEqualTo(0); + assertThat(workDuration.minutes()).isEqualTo(1); + + workDuration = WorkDuration.createFromMinutes(ONE_HOUR_IN_MINUTES, HOURS_IN_DAY); + assertThat(workDuration.days()).isEqualTo(0); + assertThat(workDuration.hours()).isEqualTo(1); + assertThat(workDuration.minutes()).isEqualTo(0); + + workDuration = WorkDuration.createFromMinutes(ONE_DAY_IN_MINUTES, HOURS_IN_DAY); + assertThat(workDuration.days()).isEqualTo(1); + assertThat(workDuration.hours()).isEqualTo(0); + assertThat(workDuration.minutes()).isEqualTo(0); + } + + @Test + public void create_from_working_long() { + // 1 minute + WorkDuration workDuration = WorkDuration.createFromLong(1L, HOURS_IN_DAY); + assertThat(workDuration.days()).isEqualTo(0); + assertThat(workDuration.hours()).isEqualTo(0); + assertThat(workDuration.minutes()).isEqualTo(1); + + // 1 hour + workDuration = WorkDuration.createFromLong(100L, HOURS_IN_DAY); + assertThat(workDuration.days()).isEqualTo(0); + assertThat(workDuration.hours()).isEqualTo(1); + assertThat(workDuration.minutes()).isEqualTo(0); + + // 1 day + workDuration = WorkDuration.createFromLong(10000L, HOURS_IN_DAY); + assertThat(workDuration.days()).isEqualTo(1); + assertThat(workDuration.hours()).isEqualTo(0); + assertThat(workDuration.minutes()).isEqualTo(0); + } + + @Test + public void convert_to_seconds() { + assertThat(WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).toMinutes()).isEqualTo(2L * ONE_MINUTE); + assertThat(WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toMinutes()).isEqualTo(2L * ONE_HOUR_IN_MINUTES); + assertThat(WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.DAYS, HOURS_IN_DAY).toMinutes()).isEqualTo(2L * ONE_DAY_IN_MINUTES); + } + + @Test + public void convert_to_working_days() { + assertThat(WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).toWorkingDays()).isEqualTo(2d / 60d / 8d); + assertThat(WorkDuration.createFromValueAndUnit(240, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).toWorkingDays()).isEqualTo(0.5); + assertThat(WorkDuration.createFromValueAndUnit(4, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toWorkingDays()).isEqualTo(0.5); + assertThat(WorkDuration.createFromValueAndUnit(8, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toWorkingDays()).isEqualTo(1d); + assertThat(WorkDuration.createFromValueAndUnit(16, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toWorkingDays()).isEqualTo(2d); + assertThat(WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.DAYS, HOURS_IN_DAY).toWorkingDays()).isEqualTo(2d); + } + + @Test + public void convert_to_working_long() { + assertThat(WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).toLong()).isEqualTo(2l); + assertThat(WorkDuration.createFromValueAndUnit(4, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toLong()).isEqualTo(400l); + assertThat(WorkDuration.createFromValueAndUnit(10, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toLong()).isEqualTo(10200l); + assertThat(WorkDuration.createFromValueAndUnit(8, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toLong()).isEqualTo(10000l); + assertThat(WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.DAYS, HOURS_IN_DAY).toLong()).isEqualTo(20000l); + } + + @Test + public void add() { + // 4h + 5h = 1d 1h + WorkDuration result = WorkDuration.createFromValueAndUnit(4, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).add(WorkDuration.createFromValueAndUnit(5, WorkDuration.UNIT.HOURS, HOURS_IN_DAY)); + assertThat(result.days()).isEqualTo(1); + assertThat(result.hours()).isEqualTo(1); + assertThat(result.minutes()).isEqualTo(0); + assertThat(result.hoursInDay()).isEqualTo(HOURS_IN_DAY); + + // 40 m + 30m = 1h 10m + result = WorkDuration.createFromValueAndUnit(40, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).add(WorkDuration.createFromValueAndUnit(30, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY)); + assertThat(result.days()).isEqualTo(0); + assertThat(result.hours()).isEqualTo(1); + assertThat(result.minutes()).isEqualTo(10); + assertThat(result.hoursInDay()).isEqualTo(HOURS_IN_DAY); + + // 10 m + 20m = 30m + assertThat(WorkDuration.createFromValueAndUnit(10, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).add( + WorkDuration.createFromValueAndUnit(20, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY) + ).minutes()).isEqualTo(30); + + assertThat(WorkDuration.createFromValueAndUnit(10, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).add(null).minutes()).isEqualTo(10); + } + + @Test + public void subtract() { + // 1d 1h - 5h = 4h + WorkDuration result = WorkDuration.create(1, 1, 0, HOURS_IN_DAY).subtract(WorkDuration.createFromValueAndUnit(5, WorkDuration.UNIT.HOURS, HOURS_IN_DAY)); + assertThat(result.days()).isEqualTo(0); + assertThat(result.hours()).isEqualTo(4); + assertThat(result.minutes()).isEqualTo(0); + assertThat(result.hoursInDay()).isEqualTo(HOURS_IN_DAY); + + // 1h 10m - 30m = 40m + result = WorkDuration.create(0, 1, 10, HOURS_IN_DAY).subtract(WorkDuration.createFromValueAndUnit(30, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY)); + assertThat(result.days()).isEqualTo(0); + assertThat(result.hours()).isEqualTo(0); + assertThat(result.minutes()).isEqualTo(40); + assertThat(result.hoursInDay()).isEqualTo(HOURS_IN_DAY); + + // 30m - 20m = 10m + assertThat(WorkDuration.createFromValueAndUnit(30, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).subtract(WorkDuration.createFromValueAndUnit(20, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY)) + .minutes()).isEqualTo(10); + + assertThat(WorkDuration.createFromValueAndUnit(10, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).subtract(null).minutes()).isEqualTo(10); + } + + @Test + public void multiply() { + // 5h * 2 = 1d 2h + WorkDuration result = WorkDuration.createFromValueAndUnit(5, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).multiply(2); + assertThat(result.days()).isEqualTo(1); + assertThat(result.hours()).isEqualTo(2); + assertThat(result.minutes()).isEqualTo(0); + assertThat(result.hoursInDay()).isEqualTo(HOURS_IN_DAY); + } + + @Test + public void test_equals_and_hashcode() throws Exception { + WorkDuration duration = WorkDuration.createFromLong(28800, HOURS_IN_DAY); + WorkDuration durationWithSameValue = WorkDuration.createFromLong(28800, HOURS_IN_DAY); + WorkDuration durationWithDifferentValue = WorkDuration.createFromLong(14400, HOURS_IN_DAY); + + assertThat(duration).isEqualTo(duration); + assertThat(durationWithSameValue).isEqualTo(duration); + assertThat(durationWithDifferentValue).isNotEqualTo(duration); + assertThat(duration).isNotEqualTo(null); + + assertThat(duration.hashCode()).isEqualTo(duration.hashCode()); + assertThat(durationWithSameValue.hashCode()).isEqualTo(duration.hashCode()); + assertThat(durationWithDifferentValue.hashCode()).isNotEqualTo(duration.hashCode()); + } + + @Test + public void test_toString() throws Exception { + assertThat(WorkDuration.createFromLong(28800, HOURS_IN_DAY).toString()).isNotNull(); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/ws/SimpleGetRequestTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/ws/SimpleGetRequestTest.java new file mode 100644 index 00000000000..fb20cf22f30 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/impl/ws/SimpleGetRequestTest.java @@ -0,0 +1,110 @@ +/* + * 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.impl.ws; + +import java.io.InputStream; +import org.junit.Test; +import org.sonar.api.server.ws.Request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.Mockito.mock; + +public class SimpleGetRequestTest { + + org.sonar.api.impl.ws.SimpleGetRequest underTest = new SimpleGetRequest(); + + @Test + public void method() { + assertThat(underTest.method()).isEqualTo("GET"); + + underTest.setParam("foo", "bar"); + assertThat(underTest.param("foo")).isEqualTo("bar"); + assertThat(underTest.param("unknown")).isNull(); + } + + @Test + public void has_param() { + assertThat(underTest.method()).isEqualTo("GET"); + + underTest.setParam("foo", "bar"); + assertThat(underTest.hasParam("foo")).isTrue(); + assertThat(underTest.hasParam("unknown")).isFalse(); + } + + @Test + public void get_part() { + InputStream inputStream = mock(InputStream.class); + underTest.setPart("key", inputStream, "filename"); + + Request.Part part = underTest.paramAsPart("key"); + assertThat(part.getInputStream()).isEqualTo(inputStream); + assertThat(part.getFileName()).isEqualTo("filename"); + + assertThat(underTest.paramAsPart("unknown")).isNull(); + } + + @Test + public void getMediaType() { + underTest.setMediaType("JSON"); + + assertThat(underTest.getMediaType()).isEqualTo("JSON"); + } + + @Test + public void multiParam_with_one_element() { + underTest.setParam("foo", "bar"); + + assertThat(underTest.multiParam("foo")).containsExactly("bar"); + } + + @Test + public void multiParam_without_any_element() { + assertThat(underTest.multiParam("42")).isEmpty(); + } + + @Test + public void getParams() { + underTest + .setParam("foo", "bar") + .setParam("fee", "beer"); + + assertThat(underTest.getParams()).containsOnly( + entry("foo", new String[] {"bar"}), + entry("fee", new String[] {"beer"})); + } + + @Test + public void header_returns_empty_if_header_is_not_present() { + assertThat(underTest.header("foo")).isEmpty(); + } + + @Test + public void header_returns_value_of_header_if_present() { + underTest.setHeader("foo", "bar"); + assertThat(underTest.header("foo")).hasValue("bar"); + } + + @Test + public void header_returns_empty_string_value_if_header_is_present_without_value() { + underTest.setHeader("foo", ""); + assertThat(underTest.header("foo")).hasValue(""); + } +} diff --git a/sonar-plugin-api-impl/src/test/resources/org/sonar/api/impl/fs/glyphicons-halflings-regular.woff b/sonar-plugin-api-impl/src/test/resources/org/sonar/api/impl/fs/glyphicons-halflings-regular.woff Binary files differnew file mode 100644 index 00000000000..2cc3e4852a5 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/resources/org/sonar/api/impl/fs/glyphicons-halflings-regular.woff |