diff options
author | Julien HENRY <julien.henry@sonarsource.com> | 2015-02-16 11:28:21 +0100 |
---|---|---|
committer | Julien HENRY <julien.henry@sonarsource.com> | 2015-02-16 18:32:02 +0100 |
commit | 2660b61c7d0c0aee191ab719bf672f7902e78c5e (patch) | |
tree | 3fc4ecdca84ae66e2d4d623e1840f32e99b5e7d1 /sonar-batch | |
parent | 1230b06fa26f9cc032c15d39f3563867d9d2914b (diff) | |
download | sonarqube-2660b61c7d0c0aee191ab719bf672f7902e78c5e.tar.gz sonarqube-2660b61c7d0c0aee191ab719bf672f7902e78c5e.zip |
SONAR-6000 Merge cpd core plugin into the batch
Diffstat (limited to 'sonar-batch')
23 files changed, 2074 insertions, 0 deletions
diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java index ea8dac835fb..31c78a9d8b7 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java +++ b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java @@ -20,6 +20,7 @@ package org.sonar.batch.bootstrap; import com.google.common.collect.Lists; +import org.sonar.batch.cpd.CpdComponents; import org.sonar.batch.design.DirectoryDsmDecorator; import org.sonar.batch.design.DirectoryTangleIndexDecorator; import org.sonar.batch.design.FileTangleIndexDecorator; @@ -89,6 +90,8 @@ public class BatchComponents { DefaultPurgeTask.class ); components.addAll(CorePropertyDefinitions.all()); + // CPD + components.addAll(CpdComponents.all()); if (!analysisMode.isMediumTest()) { components.add(MavenDependenciesSensor.class); } diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdComponents.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdComponents.java new file mode 100644 index 00000000000..6032e60b9b6 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdComponents.java @@ -0,0 +1,78 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd; + +import com.google.common.collect.ImmutableList; +import org.sonar.api.CoreProperties; +import org.sonar.api.PropertyType; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.resources.Qualifiers; +import org.sonar.batch.cpd.decorators.DuplicationDensityDecorator; +import org.sonar.batch.cpd.decorators.SumDuplicationsDecorator; +import org.sonar.batch.cpd.index.IndexFactory; + +import java.util.List; + +public final class CpdComponents { + + public static List all() { + return ImmutableList.of( + PropertyDefinition.builder(CoreProperties.CPD_CROSS_PROJECT) + .defaultValue(CoreProperties.CPD_CROSS_RPOJECT_DEFAULT_VALUE + "") + .name("Cross project duplication detection") + .description("By default, SonarQube detects duplications at sub-project level. This means that a block " + + "duplicated on two sub-projects of the same project won't be reported. Setting this parameter to \"true\" " + + "allows to detect duplicates across sub-projects and more generally across projects. Note that activating " + + "this property will slightly increase each SonarQube analysis time.") + .onQualifiers(Qualifiers.PROJECT, Qualifiers.MODULE) + .category(CoreProperties.CATEGORY_GENERAL) + .subCategory(CoreProperties.SUBCATEGORY_DUPLICATIONS) + .type(PropertyType.BOOLEAN) + .build(), + PropertyDefinition.builder(CoreProperties.CPD_SKIP_PROPERTY) + .defaultValue("false") + .name("Skip") + .description("Disable detection of duplications") + .hidden() + .category(CoreProperties.CATEGORY_GENERAL) + .subCategory(CoreProperties.SUBCATEGORY_DUPLICATIONS) + .type(PropertyType.BOOLEAN) + .build(), + PropertyDefinition.builder(CoreProperties.CPD_EXCLUSIONS) + .defaultValue("") + .name("Duplication Exclusions") + .description("Patterns used to exclude some source files from the duplication detection mechanism. " + + "See below to know how to use wildcards to specify this property.") + .onQualifiers(Qualifiers.PROJECT, Qualifiers.MODULE) + .category(CoreProperties.CATEGORY_EXCLUSIONS) + .subCategory(CoreProperties.SUBCATEGORY_DUPLICATIONS_EXCLUSIONS) + .multiValues(true) + .build(), + + CpdSensor.class, + CpdMappings.class, + SumDuplicationsDecorator.class, + DuplicationDensityDecorator.class, + IndexFactory.class, + JavaCpdEngine.class, + DefaultCpdEngine.class); + } + +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdEngine.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdEngine.java new file mode 100644 index 00000000000..45964262241 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdEngine.java @@ -0,0 +1,49 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd; + +import org.slf4j.Logger; +import org.sonar.api.BatchExtension; +import org.sonar.api.batch.sensor.SensorContext; + +public abstract class CpdEngine implements BatchExtension { + + abstract boolean isLanguageSupported(String language); + + abstract void analyse(String language, SensorContext context); + + protected void logExclusions(String[] exclusions, Logger logger) { + if (exclusions.length > 0) { + StringBuilder message = new StringBuilder("Copy-paste detection exclusions:"); + for (String exclusion : exclusions) { + message.append("\n "); + message.append(exclusion); + } + + logger.info(message.toString()); + } + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdMappings.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdMappings.java new file mode 100644 index 00000000000..ff4b7a81beb --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdMappings.java @@ -0,0 +1,51 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd; + +import org.sonar.api.BatchComponent; +import org.sonar.api.batch.CpdMapping; + +import javax.annotation.CheckForNull; + +public class CpdMappings implements BatchComponent { + + private final CpdMapping[] mappings; + + public CpdMappings(CpdMapping[] mappings) { + this.mappings = mappings; + } + + public CpdMappings() { + this(new CpdMapping[0]); + } + + @CheckForNull + public CpdMapping getMapping(String language) { + if (mappings != null) { + for (CpdMapping cpdMapping : mappings) { + if (cpdMapping.getLanguage().getKey().equals(language)) { + return cpdMapping; + } + } + } + return null; + } + +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdSensor.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdSensor.java new file mode 100644 index 00000000000..d4ba5397783 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdSensor.java @@ -0,0 +1,100 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.CoreProperties; +import org.sonar.api.batch.Phase; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.config.Settings; + +@Phase(name = Phase.Name.POST) +public class CpdSensor implements Sensor { + + private static final Logger LOG = LoggerFactory.getLogger(CpdSensor.class); + + private CpdEngine sonarEngine; + private CpdEngine sonarBridgeEngine; + private Settings settings; + private FileSystem fs; + + public CpdSensor(JavaCpdEngine sonarEngine, DefaultCpdEngine sonarBridgeEngine, Settings settings, FileSystem fs) { + this.sonarEngine = sonarEngine; + this.sonarBridgeEngine = sonarBridgeEngine; + this.settings = settings; + this.fs = fs; + } + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor.name("CPD Sensor") + .disabledInPreview(); + } + + @VisibleForTesting + CpdEngine getEngine(String language) { + if (sonarEngine.isLanguageSupported(language)) { + return sonarEngine; + } + return sonarBridgeEngine; + } + + @VisibleForTesting + boolean isSkipped(String language) { + String key = "sonar.cpd." + language + ".skip"; + if (settings.hasKey(key)) { + return settings.getBoolean(key); + } + return settings.getBoolean(CoreProperties.CPD_SKIP_PROPERTY); + } + + @Override + public void execute(SensorContext context) { + if (settings.hasKey(CoreProperties.CPD_SKIP_PROPERTY)) { + LOG.warn("\"sonar.cpd.skip\" property is deprecated and will be removed. Please set \"sonar.cpd.exclusions=**\" instead to disable duplication mechanism."); + } + + for (String language : fs.languages()) { + if (settings.hasKey("sonar.cpd." + language + ".skip")) { + LOG + .warn("\"sonar.cpd." + language + ".skip\" property is deprecated and will be removed. Please set \"sonar.cpd.exclusions=**\" instead to disable duplication mechanism."); + } + + if (isSkipped(language)) { + LOG.info("Detection of duplicated code is skipped for {}", language); + continue; + } + + CpdEngine engine = getEngine(language); + if (!engine.isLanguageSupported(language)) { + LOG.debug("Detection of duplicated code is not supported for {}", language); + continue; + } + LOG.info("{} is used for {}", engine, language); + engine.analyse(language, context); + } + } + +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/DefaultCpdEngine.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/DefaultCpdEngine.java new file mode 100644 index 00000000000..6f95c3e47f3 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/DefaultCpdEngine.java @@ -0,0 +1,193 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd; + +import org.sonar.batch.cpd.index.IndexFactory; +import org.sonar.batch.cpd.index.SonarDuplicationsIndex; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.CoreProperties; +import org.sonar.api.batch.CpdMapping; +import org.sonar.api.batch.fs.FilePredicates; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.internal.DeprecatedDefaultInputFile; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.config.Settings; +import org.sonar.api.resources.Project; +import org.sonar.api.utils.SonarException; +import org.sonar.batch.duplication.BlockCache; +import org.sonar.duplications.DuplicationPredicates; +import org.sonar.duplications.block.Block; +import org.sonar.duplications.block.FileBlocks; +import org.sonar.duplications.index.CloneGroup; +import org.sonar.duplications.internal.pmd.TokenizerBridge; + +import javax.annotation.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class DefaultCpdEngine extends CpdEngine { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultCpdEngine.class); + + /** + * Limit of time to analyse one file (in seconds). + */ + private static final int TIMEOUT = 5 * 60; + + private final IndexFactory indexFactory; + private final CpdMappings mappings; + private final FileSystem fs; + private final Settings settings; + private final BlockCache blockCache; + private final Project project; + + public DefaultCpdEngine(@Nullable Project project, IndexFactory indexFactory, CpdMappings mappings, FileSystem fs, Settings settings, BlockCache duplicationCache) { + this.project = project; + this.indexFactory = indexFactory; + this.mappings = mappings; + this.fs = fs; + this.settings = settings; + this.blockCache = duplicationCache; + } + + public DefaultCpdEngine(IndexFactory indexFactory, CpdMappings mappings, FileSystem fs, Settings settings, BlockCache duplicationCache) { + this(null, indexFactory, mappings, fs, settings, duplicationCache); + } + + @Override + public boolean isLanguageSupported(String language) { + return true; + } + + @Override + public void analyse(String languageKey, SensorContext context) { + String[] cpdExclusions = settings.getStringArray(CoreProperties.CPD_EXCLUSIONS); + logExclusions(cpdExclusions, LOG); + FilePredicates p = fs.predicates(); + List<InputFile> sourceFiles = Lists.newArrayList(fs.inputFiles(p.and( + p.hasType(InputFile.Type.MAIN), + p.hasLanguage(languageKey), + p.doesNotMatchPathPatterns(cpdExclusions) + ))); + if (sourceFiles.isEmpty()) { + return; + } + + CpdMapping mapping = mappings.getMapping(languageKey); + + // Create index + SonarDuplicationsIndex index = indexFactory.create(project, languageKey); + populateIndex(languageKey, sourceFiles, mapping, index); + + // Detect + Predicate<CloneGroup> minimumTokensPredicate = DuplicationPredicates.numberOfUnitsNotLessThan(getMinimumTokens(languageKey)); + + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try { + for (InputFile inputFile : sourceFiles) { + LOG.debug("Detection of duplications for {}", inputFile); + String resourceEffectiveKey = ((DeprecatedDefaultInputFile) inputFile).key(); + Collection<Block> fileBlocks = index.getByInputFile(inputFile, resourceEffectiveKey); + + Iterable<CloneGroup> filtered; + try { + List<CloneGroup> duplications = executorService.submit(new JavaCpdEngine.Task(index, fileBlocks)).get(TIMEOUT, TimeUnit.SECONDS); + filtered = Iterables.filter(duplications, minimumTokensPredicate); + } catch (TimeoutException e) { + filtered = null; + LOG.warn("Timeout during detection of duplications for " + inputFile, e); + } catch (InterruptedException e) { + throw new SonarException("Fail during detection of duplication for " + inputFile, e); + } catch (ExecutionException e) { + throw new SonarException("Fail during detection of duplication for " + inputFile, e); + } + + JavaCpdEngine.save(context, inputFile, filtered); + } + } finally { + executorService.shutdown(); + } + } + + private void populateIndex(String languageKey, List<InputFile> sourceFiles, CpdMapping mapping, SonarDuplicationsIndex index) { + TokenizerBridge bridge = null; + if (mapping != null) { + bridge = new TokenizerBridge(mapping.getTokenizer(), fs.encoding().name(), getBlockSize(languageKey)); + } + for (InputFile inputFile : sourceFiles) { + LOG.debug("Populating index from {}", inputFile); + String resourceEffectiveKey = ((DeprecatedDefaultInputFile) inputFile).key(); + FileBlocks fileBlocks = blockCache.byComponent(resourceEffectiveKey); + if (fileBlocks != null) { + index.insert(inputFile, fileBlocks.blocks()); + } else if (bridge != null) { + List<Block> blocks2 = bridge.chunk(resourceEffectiveKey, inputFile.file()); + index.insert(inputFile, blocks2); + } + } + } + + @VisibleForTesting + int getBlockSize(String languageKey) { + int blockSize = settings.getInt("sonar.cpd." + languageKey + ".minimumLines"); + if (blockSize == 0) { + blockSize = getDefaultBlockSize(languageKey); + } + return blockSize; + } + + @VisibleForTesting + static int getDefaultBlockSize(String languageKey) { + if ("cobol".equals(languageKey)) { + return 30; + } else if ("abap".equals(languageKey) || "natur".equals(languageKey)) { + return 20; + } else { + return 10; + } + } + + @VisibleForTesting + int getMinimumTokens(String languageKey) { + int minimumTokens = settings.getInt("sonar.cpd." + languageKey + ".minimumTokens"); + if (minimumTokens == 0) { + minimumTokens = settings.getInt(CoreProperties.CPD_MINIMUM_TOKENS_PROPERTY); + } + if (minimumTokens == 0) { + minimumTokens = 100; + } + + return minimumTokens; + } + +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/JavaCpdEngine.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/JavaCpdEngine.java new file mode 100644 index 00000000000..37bae21c422 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/JavaCpdEngine.java @@ -0,0 +1,257 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd; + +import org.sonar.batch.cpd.index.IndexFactory; +import org.sonar.batch.cpd.index.SonarDuplicationsIndex; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.CoreProperties; +import org.sonar.api.batch.fs.FilePredicates; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.internal.DeprecatedDefaultInputFile; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.duplication.DuplicationBuilder; +import org.sonar.api.batch.sensor.duplication.internal.DefaultDuplicationBuilder; +import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure; +import org.sonar.api.config.Settings; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.resources.Project; +import org.sonar.api.utils.KeyValueFormat; +import org.sonar.api.utils.SonarException; +import org.sonar.duplications.block.Block; +import org.sonar.duplications.block.BlockChunker; +import org.sonar.duplications.detector.suffixtree.SuffixTreeCloneDetectionAlgorithm; +import org.sonar.duplications.index.CloneGroup; +import org.sonar.duplications.index.CloneIndex; +import org.sonar.duplications.index.ClonePart; +import org.sonar.duplications.java.JavaStatementBuilder; +import org.sonar.duplications.java.JavaTokenProducer; +import org.sonar.duplications.statement.Statement; +import org.sonar.duplications.statement.StatementChunker; +import org.sonar.duplications.token.TokenChunker; + +import javax.annotation.Nullable; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.*; + +public class JavaCpdEngine extends CpdEngine { + + private static final Logger LOG = LoggerFactory.getLogger(JavaCpdEngine.class); + + private static final int BLOCK_SIZE = 10; + + /** + * Limit of time to analyse one file (in seconds). + */ + private static final int TIMEOUT = 5 * 60; + + private final IndexFactory indexFactory; + private final FileSystem fs; + private final Settings settings; + private final Project project; + + public JavaCpdEngine(@Nullable Project project, IndexFactory indexFactory, FileSystem fs, Settings settings) { + this.project = project; + this.indexFactory = indexFactory; + this.fs = fs; + this.settings = settings; + } + + public JavaCpdEngine(IndexFactory indexFactory, FileSystem fs, Settings settings) { + this(null, indexFactory, fs, settings); + } + + @Override + public boolean isLanguageSupported(String language) { + return "java".equals(language); + } + + @Override + public void analyse(String languageKey, SensorContext context) { + String[] cpdExclusions = settings.getStringArray(CoreProperties.CPD_EXCLUSIONS); + logExclusions(cpdExclusions, LOG); + FilePredicates p = fs.predicates(); + List<InputFile> sourceFiles = Lists.newArrayList(fs.inputFiles(p.and( + p.hasType(InputFile.Type.MAIN), + p.hasLanguage(languageKey), + p.doesNotMatchPathPatterns(cpdExclusions) + ))); + if (sourceFiles.isEmpty()) { + return; + } + SonarDuplicationsIndex index = createIndex(project, languageKey, sourceFiles); + detect(index, context, sourceFiles); + } + + private SonarDuplicationsIndex createIndex(@Nullable Project project, String language, Iterable<InputFile> sourceFiles) { + final SonarDuplicationsIndex index = indexFactory.create(project, language); + + TokenChunker tokenChunker = JavaTokenProducer.build(); + StatementChunker statementChunker = JavaStatementBuilder.build(); + BlockChunker blockChunker = new BlockChunker(BLOCK_SIZE); + + for (InputFile inputFile : sourceFiles) { + LOG.debug("Populating index from {}", inputFile); + String resourceEffectiveKey = ((DeprecatedDefaultInputFile) inputFile).key(); + + List<Statement> statements; + + Reader reader = null; + try { + reader = new InputStreamReader(new FileInputStream(inputFile.file()), fs.encoding()); + statements = statementChunker.chunk(tokenChunker.chunk(reader)); + } catch (FileNotFoundException e) { + throw new SonarException("Cannot find file " + inputFile.file(), e); + } finally { + IOUtils.closeQuietly(reader); + } + + List<Block> blocks = blockChunker.chunk(resourceEffectiveKey, statements); + index.insert(inputFile, blocks); + } + + return index; + } + + private void detect(SonarDuplicationsIndex index, org.sonar.api.batch.sensor.SensorContext context, List<InputFile> sourceFiles) { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try { + for (InputFile inputFile : sourceFiles) { + LOG.debug("Detection of duplications for {}", inputFile); + String resourceEffectiveKey = ((DeprecatedDefaultInputFile) inputFile).key(); + + Collection<Block> fileBlocks = index.getByInputFile(inputFile, resourceEffectiveKey); + + List<CloneGroup> clones; + try { + clones = executorService.submit(new Task(index, fileBlocks)).get(TIMEOUT, TimeUnit.SECONDS); + } catch (TimeoutException e) { + clones = null; + LOG.warn("Timeout during detection of duplications for " + inputFile, e); + } catch (InterruptedException e) { + throw new SonarException("Fail during detection of duplication for " + inputFile, e); + } catch (ExecutionException e) { + throw new SonarException("Fail during detection of duplication for " + inputFile, e); + } + + save(context, inputFile, clones); + } + } finally { + executorService.shutdown(); + } + } + + static class Task implements Callable<List<CloneGroup>> { + private final CloneIndex index; + private final Collection<Block> fileBlocks; + + public Task(CloneIndex index, Collection<Block> fileBlocks) { + this.index = index; + this.fileBlocks = fileBlocks; + } + + @Override + public List<CloneGroup> call() { + return SuffixTreeCloneDetectionAlgorithm.detect(index, fileBlocks); + } + } + + static void save(org.sonar.api.batch.sensor.SensorContext context, InputFile inputFile, @Nullable Iterable<CloneGroup> duplications) { + if (duplications == null || Iterables.isEmpty(duplications)) { + return; + } + Set<Integer> duplicatedLines = new HashSet<Integer>(); + int duplicatedBlocks = computeBlockAndLineCount(duplications, duplicatedLines); + Map<Integer, Integer> duplicationByLine = new HashMap<Integer, Integer>(); + for (int i = 1; i <= inputFile.lines(); i++) { + duplicationByLine.put(i, duplicatedLines.contains(i) ? 1 : 0); + } + ((DefaultMeasure<String>) context.<String>newMeasure() + .forMetric(CoreMetrics.DUPLICATION_LINES_DATA) + .onFile(inputFile) + .withValue(KeyValueFormat.format(duplicationByLine))) + .setFromCore() + .save(); + // Save + ((DefaultMeasure<Integer>) context.<Integer>newMeasure() + .forMetric(CoreMetrics.DUPLICATED_FILES) + .onFile(inputFile) + .withValue(1)) + .setFromCore() + .save(); + ((DefaultMeasure<Integer>) context.<Integer>newMeasure() + .forMetric(CoreMetrics.DUPLICATED_LINES) + .onFile(inputFile) + .withValue(duplicatedLines.size())) + .setFromCore() + .save(); + ((DefaultMeasure<Integer>) context.<Integer>newMeasure() + .forMetric(CoreMetrics.DUPLICATED_BLOCKS) + .onFile(inputFile) + .withValue(duplicatedBlocks)) + .setFromCore() + .save(); + + DuplicationBuilder builder = context.duplicationBuilder(inputFile); + for (CloneGroup duplication : duplications) { + builder.originBlock(duplication.getOriginPart().getStartLine(), duplication.getOriginPart().getEndLine()); + for (ClonePart part : duplication.getCloneParts()) { + if (!part.equals(duplication.getOriginPart())) { + ((DefaultDuplicationBuilder) builder).isDuplicatedBy(part.getResourceId(), part.getStartLine(), part.getEndLine()); + } + } + } + context.saveDuplications(inputFile, builder.build()); + } + + private static int computeBlockAndLineCount(Iterable<CloneGroup> duplications, Set<Integer> duplicatedLines) { + int duplicatedBlocks = 0; + for (CloneGroup clone : duplications) { + ClonePart origin = clone.getOriginPart(); + for (ClonePart part : clone.getCloneParts()) { + if (part.getResourceId().equals(origin.getResourceId())) { + duplicatedBlocks++; + for (int duplicatedLine = part.getStartLine(); duplicatedLine < part.getStartLine() + part.getLines(); duplicatedLine++) { + duplicatedLines.add(duplicatedLine); + } + } + } + } + return duplicatedBlocks; + } + +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/DuplicationDensityDecorator.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/DuplicationDensityDecorator.java new file mode 100644 index 00000000000..d122eeb3b2b --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/DuplicationDensityDecorator.java @@ -0,0 +1,92 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd.decorators; + +import org.sonar.api.batch.Decorator; +import org.sonar.api.batch.DecoratorContext; +import org.sonar.api.batch.DependedUpon; +import org.sonar.api.batch.DependsUpon; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Measure; +import org.sonar.api.measures.Metric; +import org.sonar.api.resources.Project; +import org.sonar.api.resources.Resource; + +import java.util.Arrays; +import java.util.List; + +public class DuplicationDensityDecorator implements Decorator { + + @DependsUpon + public List<Metric> dependsUponMetrics() { + return Arrays.<Metric>asList( + CoreMetrics.NCLOC, + CoreMetrics.COMMENT_LINES, + CoreMetrics.DUPLICATED_LINES, + CoreMetrics.LINES); + } + + @DependedUpon + public Metric generatesMetric() { + return CoreMetrics.DUPLICATED_LINES_DENSITY; + } + + @Override + public boolean shouldExecuteOnProject(Project project) { + return true; + } + + @Override + public void decorate(Resource resource, DecoratorContext context) { + Measure nbDuplicatedLines = context.getMeasure(CoreMetrics.DUPLICATED_LINES); + if (nbDuplicatedLines == null) { + return; + } + + Double divisor = getNbLinesFromLocOrNcloc(context); + if (divisor != null && divisor > 0.0) { + context.saveMeasure(CoreMetrics.DUPLICATED_LINES_DENSITY, calculate(nbDuplicatedLines.getValue(), divisor)); + } + } + + private Double getNbLinesFromLocOrNcloc(DecoratorContext context) { + Measure nbLoc = context.getMeasure(CoreMetrics.LINES); + if (nbLoc != null) { + // TODO test this branch + return nbLoc.getValue(); + } + Measure nbNcloc = context.getMeasure(CoreMetrics.NCLOC); + if (nbNcloc != null) { + Measure nbComments = context.getMeasure(CoreMetrics.COMMENT_LINES); + Double nbLines = nbNcloc.getValue(); + return nbComments != null ? nbLines + nbComments.getValue() : nbLines; + } + return null; + } + + protected Double calculate(Double dividend, Double divisor) { + Double result = 100.0 * dividend / divisor; + if (result < 100.0) { + return result; + } + return 100.0; + } + +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/SumDuplicationsDecorator.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/SumDuplicationsDecorator.java new file mode 100644 index 00000000000..af7f4b79e60 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/SumDuplicationsDecorator.java @@ -0,0 +1,55 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd.decorators; + +import org.sonar.api.batch.AbstractSumChildrenDecorator; +import org.sonar.api.batch.DependedUpon; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Metric; +import org.sonar.api.resources.Project; +import org.sonar.api.resources.Resource; +import org.sonar.api.resources.ResourceUtils; + +import java.util.Arrays; +import java.util.List; + +public class SumDuplicationsDecorator extends AbstractSumChildrenDecorator { + + @Override + @DependedUpon + public List<Metric> generatesMetrics() { + return Arrays.<Metric>asList(CoreMetrics.DUPLICATED_BLOCKS, CoreMetrics.DUPLICATED_FILES, CoreMetrics.DUPLICATED_LINES); + } + + @Override + protected boolean shouldSaveZeroIfNoChildMeasures() { + return true; + } + + @Override + public boolean shouldExecuteOnProject(Project project) { + return true; + } + + @Override + public boolean shouldDecorateResource(Resource resource) { + return !ResourceUtils.isUnitTestClass(resource); + } +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/package-info.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/package-info.java new file mode 100644 index 00000000000..51735f6c8dc --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/package-info.java @@ -0,0 +1,25 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd.decorators; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/index/DbDuplicationsIndex.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/DbDuplicationsIndex.java new file mode 100644 index 00000000000..4318e0fa8ad --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/DbDuplicationsIndex.java @@ -0,0 +1,137 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd.index; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.database.DatabaseSession; +import org.sonar.api.database.model.Snapshot; +import org.sonar.api.resources.Project; +import org.sonar.batch.index.ResourceCache; +import org.sonar.core.duplication.DuplicationDao; +import org.sonar.core.duplication.DuplicationUnitDto; +import org.sonar.duplications.block.Block; +import org.sonar.duplications.block.ByteArray; + +import javax.persistence.Query; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class DbDuplicationsIndex { + + private static final String RESOURCE_ID = "resourceId"; + private static final String LAST = "last"; + + private final Map<ByteArray, Collection<Block>> cache = Maps.newHashMap(); + + private final int currentProjectSnapshotId; + private final Integer lastSnapshotId; + private final String languageKey; + private final DuplicationDao dao; + private final DatabaseSession session; + private final ResourceCache resourceCache; + + public DbDuplicationsIndex(Project currentProject, DuplicationDao dao, + String language, DatabaseSession session, ResourceCache resourceCache) { + this.dao = dao; + this.session = session; + this.resourceCache = resourceCache; + Snapshot lastSnapshot = getLastSnapshot(currentProject.getId()); + this.currentProjectSnapshotId = resourceCache.get(currentProject.getEffectiveKey()).snapshotId(); + this.lastSnapshotId = lastSnapshot == null ? null : lastSnapshot.getId(); + this.languageKey = language; + } + + private Snapshot getLastSnapshot(int resourceId) { + String hql = "SELECT s FROM " + Snapshot.class.getSimpleName() + " s WHERE s.last=:last AND s.resourceId=:resourceId"; + Query query = session.createQuery(hql); + query.setParameter(LAST, true); + query.setParameter(RESOURCE_ID, resourceId); + return session.getSingleResult(query, null); + } + + int getSnapshotIdFor(InputFile inputFile) { + return resourceCache.get(((DefaultInputFile) inputFile).key()).snapshotId(); + } + + public void prepareCache(InputFile inputFile) { + int resourceSnapshotId = getSnapshotIdFor(inputFile); + List<DuplicationUnitDto> units = dao.selectCandidates(resourceSnapshotId, lastSnapshotId, languageKey); + cache.clear(); + // TODO Godin: maybe remove conversion of units to blocks? + for (DuplicationUnitDto unit : units) { + String hash = unit.getHash(); + String resourceKey = unit.getResourceKey(); + int indexInFile = unit.getIndexInFile(); + int startLine = unit.getStartLine(); + int endLine = unit.getEndLine(); + + // TODO Godin: in fact we could work directly with id instead of key - this will allow to decrease memory consumption + Block block = Block.builder() + .setResourceId(resourceKey) + .setBlockHash(new ByteArray(hash)) + .setIndexInFile(indexInFile) + .setLines(startLine, endLine) + .build(); + + // Group blocks by hash + Collection<Block> sameHash = cache.get(block.getBlockHash()); + if (sameHash == null) { + sameHash = Lists.newArrayList(); + cache.put(block.getBlockHash(), sameHash); + } + sameHash.add(block); + } + } + + public Collection<Block> getByHash(ByteArray hash) { + Collection<Block> result = cache.get(hash); + if (result != null) { + return result; + } else { + return Collections.emptyList(); + } + } + + public void insert(InputFile inputFile, Collection<Block> blocks) { + int resourceSnapshotId = getSnapshotIdFor(inputFile); + + // TODO Godin: maybe remove conversion of blocks to units? + List<DuplicationUnitDto> units = Lists.newArrayList(); + for (Block block : blocks) { + DuplicationUnitDto unit = new DuplicationUnitDto( + currentProjectSnapshotId, + resourceSnapshotId, + block.getBlockHash().toString(), + block.getIndexInFile(), + block.getStartLine(), + block.getEndLine()); + units.add(unit); + } + + dao.insert(units); + } + +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/index/IndexFactory.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/IndexFactory.java new file mode 100644 index 00000000000..a11f94a0fde --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/IndexFactory.java @@ -0,0 +1,90 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd.index; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.BatchComponent; +import org.sonar.api.CoreProperties; +import org.sonar.api.config.Settings; +import org.sonar.api.database.DatabaseSession; +import org.sonar.api.resources.Project; +import org.sonar.batch.bootstrap.DefaultAnalysisMode; +import org.sonar.batch.index.ResourceCache; +import org.sonar.core.duplication.DuplicationDao; + +import javax.annotation.Nullable; + +public class IndexFactory implements BatchComponent { + + private static final Logger LOG = LoggerFactory.getLogger(IndexFactory.class); + + private final Settings settings; + private final DuplicationDao dao; + private final DefaultAnalysisMode mode; + private final DatabaseSession session; + private final ResourceCache resourceCache; + + public IndexFactory(DefaultAnalysisMode mode, Settings settings, @Nullable DuplicationDao dao, @Nullable DatabaseSession session, ResourceCache resourceCache) { + this.mode = mode; + this.settings = settings; + this.dao = dao; + this.session = session; + this.resourceCache = resourceCache; + } + + /** + * Used by new sensor mode + */ + public IndexFactory(DefaultAnalysisMode mode, Settings settings, ResourceCache resourceCache) { + this(mode, settings, null, null, resourceCache); + } + + public SonarDuplicationsIndex create(@Nullable Project project, String languageKey) { + if (verifyCrossProject(project, LOG) && dao != null && session != null) { + return new SonarDuplicationsIndex(new DbDuplicationsIndex(project, dao, languageKey, session, resourceCache)); + } + return new SonarDuplicationsIndex(); + } + + @VisibleForTesting + boolean verifyCrossProject(@Nullable Project project, Logger logger) { + boolean crossProject = false; + + if (settings.getBoolean(CoreProperties.CPD_CROSS_PROJECT)) { + if (mode.isPreview()) { + logger.info("Cross-project analysis disabled. Not supported in preview mode."); + } else if (StringUtils.isNotBlank(settings.getString(CoreProperties.PROJECT_BRANCH_PROPERTY))) { + logger.info("Cross-project analysis disabled. Not supported on project branches."); + } else if (project == null) { + // New sensor mode + logger.info("Cross-project analysis disabled. Not supported in new sensor mode."); + } else { + logger.info("Cross-project analysis enabled"); + crossProject = true; + } + } else { + logger.info("Cross-project analysis disabled"); + } + return crossProject; + } +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/index/SonarDuplicationsIndex.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/SonarDuplicationsIndex.java new file mode 100644 index 00000000000..dacedc06b74 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/SonarDuplicationsIndex.java @@ -0,0 +1,83 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd.index; + +import com.google.common.collect.Lists; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.duplications.block.Block; +import org.sonar.duplications.block.ByteArray; +import org.sonar.duplications.index.AbstractCloneIndex; +import org.sonar.duplications.index.CloneIndex; +import org.sonar.duplications.index.PackedMemoryCloneIndex; + +import java.util.Collection; +import java.util.List; + +public class SonarDuplicationsIndex extends AbstractCloneIndex { + + private final CloneIndex mem = new PackedMemoryCloneIndex(); + private final DbDuplicationsIndex db; + + public SonarDuplicationsIndex() { + this.db = null; + } + + public SonarDuplicationsIndex(DbDuplicationsIndex db) { + this.db = db; + } + + public void insert(InputFile inputFile, Collection<Block> blocks) { + for (Block block : blocks) { + mem.insert(block); + } + if (db != null) { + db.insert(inputFile, blocks); + } + } + + public Collection<Block> getByInputFile(InputFile inputFile, String resourceKey) { + if (db != null) { + db.prepareCache(inputFile); + } + return mem.getByResourceId(resourceKey); + } + + @Override + public Collection<Block> getBySequenceHash(ByteArray hash) { + if (db == null) { + return mem.getBySequenceHash(hash); + } else { + List<Block> result = Lists.newArrayList(mem.getBySequenceHash(hash)); + result.addAll(db.getByHash(hash)); + return result; + } + } + + @Override + public Collection<Block> getByResourceId(String resourceId) { + throw new UnsupportedOperationException(); + } + + @Override + public void insert(Block block) { + throw new UnsupportedOperationException(); + } + +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/index/package-info.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/package-info.java new file mode 100644 index 00000000000..5d53db6190b --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/package-info.java @@ -0,0 +1,25 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd.index; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/package-info.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/package-info.java new file mode 100644 index 00000000000..8be46d698af --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/package-info.java @@ -0,0 +1,25 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-batch/src/test/java/org/sonar/batch/cpd/CpdComponentsTest.java b/sonar-batch/src/test/java/org/sonar/batch/cpd/CpdComponentsTest.java new file mode 100644 index 00000000000..90632426ee3 --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/cpd/CpdComponentsTest.java @@ -0,0 +1,32 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CpdComponentsTest { + + @Test + public void getExtensions() { + assertThat(CpdComponents.all()).hasSize(10); + } +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/cpd/CpdSensorTest.java b/sonar-batch/src/test/java/org/sonar/batch/cpd/CpdSensorTest.java new file mode 100644 index 00000000000..2c6f195b880 --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/cpd/CpdSensorTest.java @@ -0,0 +1,91 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd; + +import org.sonar.batch.cpd.CpdMappings; +import org.sonar.batch.cpd.CpdComponents; +import org.sonar.batch.cpd.CpdSensor; +import org.sonar.batch.cpd.DefaultCpdEngine; +import org.sonar.batch.cpd.JavaCpdEngine; +import org.sonar.batch.cpd.index.IndexFactory; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.fs.internal.DefaultFileSystem; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.api.config.Settings; +import org.sonar.api.resources.Java; +import org.sonar.batch.duplication.BlockCache; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class CpdSensorTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + JavaCpdEngine sonarEngine; + DefaultCpdEngine sonarBridgeEngine; + CpdSensor sensor; + Settings settings; + + @Before + public void setUp() throws IOException { + IndexFactory indexFactory = mock(IndexFactory.class); + sonarEngine = new JavaCpdEngine(indexFactory, null, null); + sonarBridgeEngine = new DefaultCpdEngine(indexFactory, new CpdMappings(), null, null, mock(BlockCache.class)); + settings = new Settings(new PropertyDefinitions(CpdComponents.class)); + + DefaultFileSystem fs = new DefaultFileSystem(temp.newFolder().toPath()); + sensor = new CpdSensor(sonarEngine, sonarBridgeEngine, settings, fs); + } + + @Test + public void test_global_skip() { + settings.setProperty("sonar.cpd.skip", true); + assertThat(sensor.isSkipped(Java.KEY)).isTrue(); + } + + @Test + public void should_not_skip_by_default() { + assertThat(sensor.isSkipped(Java.KEY)).isFalse(); + } + + @Test + public void should_skip_by_language() { + settings.setProperty("sonar.cpd.skip", false); + settings.setProperty("sonar.cpd.php.skip", true); + + assertThat(sensor.isSkipped("php")).isTrue(); + assertThat(sensor.isSkipped(Java.KEY)).isFalse(); + } + + @Test + public void test_engine() { + assertThat(sensor.getEngine(Java.KEY)).isSameAs(sonarEngine); + assertThat(sensor.getEngine("PHP")).isSameAs(sonarBridgeEngine); + } + +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/cpd/DefaultCpdEngineTest.java b/sonar-batch/src/test/java/org/sonar/batch/cpd/DefaultCpdEngineTest.java new file mode 100644 index 00000000000..9ce86ce3ee4 --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/cpd/DefaultCpdEngineTest.java @@ -0,0 +1,110 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd; + +import org.sonar.batch.cpd.DefaultCpdEngine; + +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.sonar.api.config.Settings; +import org.sonar.api.resources.Project; +import org.sonar.batch.duplication.BlockCache; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class DefaultCpdEngineTest { + + private DefaultCpdEngine engine; + private Settings settings; + + @Before + public void init() { + settings = new Settings(); + engine = new DefaultCpdEngine(null, null, null, settings, mock(BlockCache.class)); + } + + @Test + public void shouldLogExclusions() { + Logger logger = mock(Logger.class); + engine.logExclusions(new String[0], logger); + verify(logger, never()).info(anyString()); + + logger = mock(Logger.class); + engine.logExclusions(new String[] {"Foo*", "**/Bar*"}, logger); + + String message = "Copy-paste detection exclusions:" + + "\n Foo*" + + "\n **/Bar*"; + verify(logger, times(1)).info(message); + } + + @Test + public void shouldReturnDefaultBlockSize() { + assertThat(DefaultCpdEngine.getDefaultBlockSize("cobol")).isEqualTo(30); + assertThat(DefaultCpdEngine.getDefaultBlockSize("natur")).isEqualTo(20); + assertThat(DefaultCpdEngine.getDefaultBlockSize("abap")).isEqualTo(20); + assertThat(DefaultCpdEngine.getDefaultBlockSize("other")).isEqualTo(10); + } + + @Test + public void defaultBlockSize() { + + assertThat(engine.getBlockSize("java")).isEqualTo(10); + } + + @Test + public void blockSizeForCobol() { + settings.setProperty("sonar.cpd.cobol.minimumLines", "42"); + + assertThat(engine.getBlockSize("cobol")).isEqualTo(42); + } + + @Test + public void defaultMinimumTokens() { + assertThat(engine.getMinimumTokens("java")).isEqualTo(100); + } + + @Test + public void generalMinimumTokens() { + settings.setProperty("sonar.cpd.minimumTokens", 33); + + assertThat(engine.getMinimumTokens("java")).isEqualTo(33); + } + + @Test + public void minimumTokensByLanguage() { + settings.setProperty("sonar.cpd.java.minimumTokens", "42"); + settings.setProperty("sonar.cpd.php.minimumTokens", "33"); + assertThat(engine.getMinimumTokens("java")).isEqualTo(42); + + settings.setProperty("sonar.cpd.java.minimumTokens", "42"); + settings.setProperty("sonar.cpd.php.minimumTokens", "33"); + assertThat(engine.getMinimumTokens("php")).isEqualTo(33); + } + + private static Project newProject(String key) { + return new Project(key).setAnalysisType(Project.AnalysisType.DYNAMIC); + } +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/cpd/JavaCpdEngineTest.java b/sonar-batch/src/test/java/org/sonar/batch/cpd/JavaCpdEngineTest.java new file mode 100644 index 00000000000..711f70f9be4 --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/cpd/JavaCpdEngineTest.java @@ -0,0 +1,166 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd; + +import org.sonar.batch.cpd.JavaCpdEngine; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.InOrder; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.internal.DeprecatedDefaultInputFile; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorStorage; +import org.sonar.api.batch.sensor.duplication.DuplicationGroup; +import org.sonar.api.batch.sensor.duplication.internal.DefaultDuplicationBuilder; +import org.sonar.api.batch.sensor.measure.Measure; +import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.duplications.index.CloneGroup; +import org.sonar.duplications.index.ClonePart; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +public class JavaCpdEngineTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + SensorContext context = mock(SensorContext.class); + DeprecatedDefaultInputFile inputFile; + private DefaultDuplicationBuilder duplicationBuilder; + private SensorStorage storage = mock(SensorStorage.class); + + @Before + public void before() throws IOException { + when(context.newMeasure()).then(new Answer<Measure>() { + @Override + public Measure answer(InvocationOnMock invocation) throws Throwable { + return new DefaultMeasure(storage); + } + }); + inputFile = new DeprecatedDefaultInputFile("foo", "src/main/java/Foo.java"); + duplicationBuilder = spy(new DefaultDuplicationBuilder(inputFile)); + when(context.duplicationBuilder(any(InputFile.class))).thenReturn(duplicationBuilder); + inputFile.setModuleBaseDir(temp.newFolder().toPath()); + } + + @SuppressWarnings("unchecked") + @Test + public void testNothingToSave() { + JavaCpdEngine.save(context, inputFile, null); + JavaCpdEngine.save(context, inputFile, Collections.EMPTY_LIST); + + verifyZeroInteractions(context); + } + + @Test + public void testOneSimpleDuplicationBetweenTwoFiles() { + inputFile.setLines(5); + List<CloneGroup> groups = Arrays.asList(newCloneGroup(new ClonePart("key1", 0, 2, 4), new ClonePart("key2", 0, 15, 17))); + JavaCpdEngine.save(context, inputFile, groups); + + verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_FILES).onFile(inputFile).withValue(1)); + verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_BLOCKS).onFile(inputFile).withValue(1)); + verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_LINES).onFile(inputFile).withValue(3)); + verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATION_LINES_DATA).onFile(inputFile).withValue("1=0;2=1;3=1;4=1;5=0")); + + InOrder inOrder = Mockito.inOrder(duplicationBuilder); + inOrder.verify(duplicationBuilder).originBlock(2, 4); + inOrder.verify(duplicationBuilder).isDuplicatedBy("key2", 15, 17); + inOrder.verify(duplicationBuilder).build(); + } + + @Test + public void testDuplicationOnSameFile() throws Exception { + List<CloneGroup> groups = Arrays.asList(newCloneGroup(new ClonePart("key1", 0, 5, 204), new ClonePart("key1", 0, 215, 414))); + JavaCpdEngine.save(context, inputFile, groups); + + verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_FILES).onFile(inputFile).withValue(1)); + verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_BLOCKS).onFile(inputFile).withValue(2)); + verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_LINES).onFile(inputFile).withValue(400)); + + InOrder inOrder = Mockito.inOrder(duplicationBuilder); + inOrder.verify(duplicationBuilder).originBlock(5, 204); + inOrder.verify(duplicationBuilder).isDuplicatedBy("key1", 215, 414); + inOrder.verify(duplicationBuilder).build(); + } + + @Test + public void testOneDuplicatedGroupInvolvingMoreThanTwoFiles() throws Exception { + List<CloneGroup> groups = Arrays.asList(newCloneGroup(new ClonePart("key1", 0, 5, 204), new ClonePart("key2", 0, 15, 214), new ClonePart("key3", 0, 25, 224))); + JavaCpdEngine.save(context, inputFile, groups); + + verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_FILES).onFile(inputFile).withValue(1)); + verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_BLOCKS).onFile(inputFile).withValue(1)); + verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_LINES).onFile(inputFile).withValue(200)); + + InOrder inOrder = Mockito.inOrder(duplicationBuilder); + inOrder.verify(duplicationBuilder).originBlock(5, 204); + inOrder.verify(duplicationBuilder).isDuplicatedBy("key2", 15, 214); + inOrder.verify(duplicationBuilder).isDuplicatedBy("key3", 25, 224); + inOrder.verify(duplicationBuilder).build(); + + verify(context).saveDuplications(inputFile, Arrays.asList( + new DuplicationGroup(new DuplicationGroup.Block("foo:src/main/java/Foo.java", 5, 200)) + .addDuplicate(new DuplicationGroup.Block("key2", 15, 200)) + .addDuplicate(new DuplicationGroup.Block("key3", 25, 200)) + )); + } + + @Test + public void testTwoDuplicatedGroupsInvolvingThreeFiles() throws Exception { + List<CloneGroup> groups = Arrays.asList( + newCloneGroup(new ClonePart("key1", 0, 5, 204), new ClonePart("key2", 0, 15, 214)), + newCloneGroup(new ClonePart("key1", 0, 15, 214), new ClonePart("key3", 0, 15, 214))); + JavaCpdEngine.save(context, inputFile, groups); + + verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_FILES).onFile(inputFile).withValue(1)); + verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_BLOCKS).onFile(inputFile).withValue(2)); + verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_LINES).onFile(inputFile).withValue(210)); + + InOrder inOrder = Mockito.inOrder(duplicationBuilder); + inOrder.verify(duplicationBuilder).originBlock(5, 204); + inOrder.verify(duplicationBuilder).isDuplicatedBy("key2", 15, 214); + inOrder.verify(duplicationBuilder).originBlock(15, 214); + inOrder.verify(duplicationBuilder).isDuplicatedBy("key3", 15, 214); + inOrder.verify(duplicationBuilder).build(); + } + + private CloneGroup newCloneGroup(ClonePart... parts) { + return CloneGroup.builder().setLength(0).setOrigin(parts[0]).setParts(Arrays.asList(parts)).build(); + } + +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/cpd/decorators/DuplicationDensityDecoratorTest.java b/sonar-batch/src/test/java/org/sonar/batch/cpd/decorators/DuplicationDensityDecoratorTest.java new file mode 100644 index 00000000000..629a1cf6d5d --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/cpd/decorators/DuplicationDensityDecoratorTest.java @@ -0,0 +1,82 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd.decorators; + +import org.sonar.batch.cpd.decorators.DuplicationDensityDecorator; + +import org.junit.Test; +import static org.mockito.Mockito.*; +import org.sonar.api.batch.DecoratorContext; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Measure; + +public class DuplicationDensityDecoratorTest { + + @Test + public void densityIsBalancedByNclocAndCommentLines() { + DecoratorContext context = mock(DecoratorContext.class); + when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 40.0)); + when(context.getMeasure(CoreMetrics.COMMENT_LINES)).thenReturn(new Measure(CoreMetrics.COMMENT_LINES, 10.0)); + when(context.getMeasure(CoreMetrics.DUPLICATED_LINES)).thenReturn(new Measure(CoreMetrics.DUPLICATED_LINES, 10.0)); + + DuplicationDensityDecorator decorator = new DuplicationDensityDecorator(); + decorator.decorate(null, context); + + verify(context).saveMeasure(CoreMetrics.DUPLICATED_LINES_DENSITY, 20.0); + } + + + @Test + public void densityEvenIfNoComments() { + DecoratorContext context = mock(DecoratorContext.class); + when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 40.0)); + when(context.getMeasure(CoreMetrics.DUPLICATED_LINES)).thenReturn(new Measure(CoreMetrics.DUPLICATED_LINES, 10.0)); + + DuplicationDensityDecorator decorator = new DuplicationDensityDecorator(); + decorator.decorate(null, context); + + verify(context).saveMeasure(CoreMetrics.DUPLICATED_LINES_DENSITY, 25.0); + } + + @Test + public void noDensityIfNoDuplicationMeasure() { + DecoratorContext context = mock(DecoratorContext.class); + when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 45.0)); + + DuplicationDensityDecorator decorator = new DuplicationDensityDecorator(); + decorator.decorate(null, context); + + verify(context, never()).saveMeasure(eq(CoreMetrics.DUPLICATED_LINES_DENSITY), anyDouble()); + } + + @Test + public void noDensityWhenZeroNclocAndComments() { + DecoratorContext context = mock(DecoratorContext.class); + when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 0.0)); + when(context.getMeasure(CoreMetrics.DUPLICATED_LINES)).thenReturn(new Measure(CoreMetrics.COMMENT_LINES, 0.0)); + when(context.getMeasure(CoreMetrics.DUPLICATED_LINES)).thenReturn(new Measure(CoreMetrics.DUPLICATED_LINES, 10.0)); + + DuplicationDensityDecorator decorator = new DuplicationDensityDecorator(); + decorator.decorate(null, context); + + verify(context, never()).saveMeasure(eq(CoreMetrics.DUPLICATED_LINES_DENSITY), anyDouble()); + } + +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/cpd/decorators/SumDuplicationsDecoratorTest.java b/sonar-batch/src/test/java/org/sonar/batch/cpd/decorators/SumDuplicationsDecoratorTest.java new file mode 100644 index 00000000000..a86cfb423ed --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/cpd/decorators/SumDuplicationsDecoratorTest.java @@ -0,0 +1,71 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd.decorators; + +import org.sonar.batch.cpd.decorators.SumDuplicationsDecorator; + +import org.junit.Test; +import org.sonar.api.batch.DecoratorContext; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Measure; +import org.sonar.api.resources.File; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.test.IsMeasure; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class SumDuplicationsDecoratorTest { + + @Test + public void parapets() { + SumDuplicationsDecorator decorator = new SumDuplicationsDecorator(); + assertThat(decorator.generatesMetrics().size(), greaterThan(0)); + assertThat(decorator.shouldSaveZeroIfNoChildMeasures(), is(true)); + } + + @Test + public void doNotSetDuplicationsOnUnitTests() { + SumDuplicationsDecorator decorator = new SumDuplicationsDecorator(); + File unitTest = File.create("org/foo/BarTest.java"); + unitTest.setQualifier(Qualifiers.UNIT_TEST_FILE); + DecoratorContext context = mock(DecoratorContext.class); + + decorator.decorate(unitTest, context); + + verify(context, never()).saveMeasure(any(Measure.class)); + } + + @Test + public void saveZeroIfNoDuplications() { + SumDuplicationsDecorator decorator = new SumDuplicationsDecorator(); + File file = File.create("org/foo/BarTest.java"); + DecoratorContext context = mock(DecoratorContext.class); + + decorator.decorate(file, context); + + verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.DUPLICATED_LINES, 0.0))); + } +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/cpd/index/IndexFactoryTest.java b/sonar-batch/src/test/java/org/sonar/batch/cpd/index/IndexFactoryTest.java new file mode 100644 index 00000000000..d60ace37875 --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/cpd/index/IndexFactoryTest.java @@ -0,0 +1,86 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cpd.index; + +import org.sonar.batch.cpd.index.IndexFactory; + +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.sonar.api.CoreProperties; +import org.sonar.api.config.Settings; +import org.sonar.api.database.DatabaseSession; +import org.sonar.api.resources.Project; +import org.sonar.batch.bootstrap.DefaultAnalysisMode; +import org.sonar.batch.index.ResourceCache; +import org.sonar.core.duplication.DuplicationDao; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class IndexFactoryTest { + + Project project; + Settings settings; + IndexFactory factory; + Logger logger; + private DefaultAnalysisMode analysisMode; + + @Before + public void setUp() { + project = new Project("foo"); + settings = new Settings(); + analysisMode = mock(DefaultAnalysisMode.class); + factory = new IndexFactory(analysisMode, settings, mock(DuplicationDao.class), mock(DatabaseSession.class), new ResourceCache()); + logger = mock(Logger.class); + } + + @Test + public void crossProjectEnabled() { + settings.setProperty(CoreProperties.CPD_CROSS_PROJECT, "true"); + assertThat(factory.verifyCrossProject(project, logger)).isTrue(); + verify(logger).info("Cross-project analysis enabled"); + } + + @Test + public void noCrossProjectWithBranch() { + settings.setProperty(CoreProperties.CPD_CROSS_PROJECT, "true"); + settings.setProperty(CoreProperties.PROJECT_BRANCH_PROPERTY, "branch"); + assertThat(factory.verifyCrossProject(project, logger)).isFalse(); + verify(logger).info("Cross-project analysis disabled. Not supported on project branches."); + } + + @Test + public void cross_project_should_be_disabled_on_preview() { + when(analysisMode.isPreview()).thenReturn(true); + settings.setProperty(CoreProperties.CPD_CROSS_PROJECT, "true"); + assertThat(factory.verifyCrossProject(project, logger)).isFalse(); + verify(logger).info("Cross-project analysis disabled. Not supported in preview mode."); + } + + @Test + public void crossProjectDisabled() { + settings.setProperty(CoreProperties.CPD_CROSS_PROJECT, "false"); + assertThat(factory.verifyCrossProject(project, logger)).isFalse(); + verify(logger).info("Cross-project analysis disabled"); + } + +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/mediumtest/cpd/CpdMediumTest.java b/sonar-batch/src/test/java/org/sonar/batch/mediumtest/cpd/CpdMediumTest.java new file mode 100644 index 00000000000..e5e6f6c1a1c --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/mediumtest/cpd/CpdMediumTest.java @@ -0,0 +1,173 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.mediumtest.cpd; + +import com.google.common.collect.ImmutableMap; +import org.apache.commons.io.FileUtils; +import org.junit.After; +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.fs.InputFile; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.sensor.duplication.DuplicationGroup; +import org.sonar.batch.mediumtest.BatchMediumTester; +import org.sonar.batch.mediumtest.TaskResult; +import org.sonar.xoo.XooPlugin; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CpdMediumTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + public BatchMediumTester tester = BatchMediumTester.builder() + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way") + .build(); + + private File baseDir; + + private ImmutableMap.Builder<String, String> builder; + + @Before + public void prepare() throws IOException { + tester.start(); + + baseDir = temp.newFolder(); + + builder = ImmutableMap.<String, String>builder() + .put("sonar.task", "scan") + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project"); + } + + @After + public void stop() { + tester.stop(); + } + + @Test + public void testCrossFileDuplications() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + String duplicatedStuff = "Sample xoo\ncontent\nfoo\nbar\ntoto\ntiti\nfoo\nbar\ntoto\ntiti\nbar\ntoto\ntiti\nfoo\nbar\ntoto\ntiti"; + + File xooFile1 = new File(srcDir, "sample1.xoo"); + FileUtils.write(xooFile1, duplicatedStuff); + + File xooFile2 = new File(srcDir, "sample2.xoo"); + FileUtils.write(xooFile2, duplicatedStuff); + + TaskResult result = tester.newTask() + .properties(builder + .put("sonar.sources", "src") + .put("sonar.cpd.xoo.minimumTokens", "10") + .put("sonar.verbose", "true") + .build()) + .start(); + + assertThat(result.inputFiles()).hasSize(2); + + // 5 measures per file + quality profile measure + assertThat(result.measures()).hasSize(11); + + InputFile inputFile1 = result.inputFile("src/sample1.xoo"); + InputFile inputFile2 = result.inputFile("src/sample2.xoo"); + // One clone group on each file + List<DuplicationGroup> duplicationGroupsFile1 = result.duplicationsFor(inputFile1); + assertThat(duplicationGroupsFile1).hasSize(1); + + DuplicationGroup cloneGroupFile1 = duplicationGroupsFile1.get(0); + assertThat(cloneGroupFile1.duplicates()).hasSize(1); + assertThat(cloneGroupFile1.originBlock().startLine()).isEqualTo(1); + assertThat(cloneGroupFile1.originBlock().length()).isEqualTo(17); + assertThat(cloneGroupFile1.originBlock().resourceKey()).isEqualTo(((DefaultInputFile) inputFile1).key()); + assertThat(cloneGroupFile1.duplicates()).hasSize(1); + assertThat(cloneGroupFile1.duplicates().get(0).resourceKey()).isEqualTo(((DefaultInputFile) inputFile2).key()); + + List<DuplicationGroup> duplicationGroupsFile2 = result.duplicationsFor(inputFile2); + assertThat(duplicationGroupsFile2).hasSize(1); + + DuplicationGroup cloneGroupFile2 = duplicationGroupsFile2.get(0); + assertThat(cloneGroupFile2.duplicates()).hasSize(1); + assertThat(cloneGroupFile2.originBlock().startLine()).isEqualTo(1); + assertThat(cloneGroupFile2.originBlock().length()).isEqualTo(17); + assertThat(cloneGroupFile2.originBlock().resourceKey()).isEqualTo(((DefaultInputFile) inputFile2).key()); + assertThat(cloneGroupFile2.duplicates()).hasSize(1); + assertThat(cloneGroupFile2.duplicates().get(0).resourceKey()).isEqualTo(((DefaultInputFile) inputFile1).key()); + } + + @Test + public void testIntraFileDuplications() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + String content = "Sample xoo\ncontent\nfoo\nbar\nSample xoo\ncontent\n"; + + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, content); + + TaskResult result = tester.newTask() + .properties(builder + .put("sonar.sources", "src") + .put("sonar.cpd.xoo.minimumTokens", "2") + .put("sonar.cpd.xoo.minimumLines", "2") + .put("sonar.verbose", "true") + .build()) + .start(); + + // 5 measures per file + QP measure + assertThat(result.measures()).hasSize(6); + + InputFile inputFile = result.inputFile("src/sample.xoo"); + // One clone group + List<DuplicationGroup> duplicationGroups = result.duplicationsFor(inputFile); + assertThat(duplicationGroups).hasSize(1); + + DuplicationGroup cloneGroup = duplicationGroups.get(0); + assertThat(cloneGroup.duplicates()).hasSize(1); + assertThat(cloneGroup.originBlock().startLine()).isEqualTo(1); + assertThat(cloneGroup.originBlock().length()).isEqualTo(2); + assertThat(cloneGroup.duplicates()).hasSize(1); + assertThat(cloneGroup.duplicates().get(0).startLine()).isEqualTo(5); + assertThat(cloneGroup.duplicates().get(0).length()).isEqualTo(2); + + // assertThat(result.measures()).contains(new DefaultMeasure<String>() + // .forMetric(CoreMetrics.DUPLICATION_LINES_DATA) + // .onFile(inputFile) + // .withValue("1=1;2=1;3=0;4=0;5=1;6=1;7=0")); + } + +} |