001/* 002 * SonarQube 003 * Copyright (C) 2009-2016 SonarSource SA 004 * mailto:contact AT sonarsource DOT com 005 * 006 * This program is free software; you can redistribute it and/or 007 * modify it under the terms of the GNU Lesser General Public 008 * License as published by the Free Software Foundation; either 009 * version 3 of the License, or (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 * Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public License 017 * along with this program; if not, write to the Free Software Foundation, 018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 019 */ 020package org.sonar.api.batch.sensor.internal; 021 022import com.google.common.annotations.Beta; 023import java.io.File; 024import java.io.Serializable; 025import java.nio.file.Path; 026import java.util.ArrayList; 027import java.util.Collection; 028import java.util.Collections; 029import java.util.List; 030import java.util.Map; 031import java.util.Set; 032import javax.annotation.CheckForNull; 033import org.sonar.api.SonarQubeVersion; 034import org.sonar.api.batch.fs.InputModule; 035import org.sonar.api.batch.fs.TextRange; 036import org.sonar.api.batch.fs.internal.DefaultFileSystem; 037import org.sonar.api.batch.fs.internal.DefaultInputModule; 038import org.sonar.api.batch.fs.internal.DefaultTextPointer; 039import org.sonar.api.batch.rule.ActiveRules; 040import org.sonar.api.batch.rule.internal.ActiveRulesBuilder; 041import org.sonar.api.batch.sensor.Sensor; 042import org.sonar.api.batch.sensor.SensorContext; 043import org.sonar.api.batch.sensor.coverage.CoverageType; 044import org.sonar.api.batch.sensor.coverage.NewCoverage; 045import org.sonar.api.batch.sensor.coverage.internal.DefaultCoverage; 046import org.sonar.api.batch.sensor.cpd.NewCpdTokens; 047import org.sonar.api.batch.sensor.cpd.internal.DefaultCpdTokens; 048import org.sonar.api.batch.sensor.highlighting.NewHighlighting; 049import org.sonar.api.batch.sensor.highlighting.TypeOfText; 050import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting; 051import org.sonar.api.batch.sensor.highlighting.internal.SyntaxHighlightingRule; 052import org.sonar.api.batch.sensor.issue.Issue; 053import org.sonar.api.batch.sensor.issue.NewIssue; 054import org.sonar.api.batch.sensor.issue.internal.DefaultIssue; 055import org.sonar.api.batch.sensor.measure.Measure; 056import org.sonar.api.batch.sensor.measure.NewMeasure; 057import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure; 058import org.sonar.api.batch.sensor.symbol.NewSymbolTable; 059import org.sonar.api.batch.sensor.symbol.internal.DefaultSymbolTable; 060import org.sonar.api.config.Settings; 061import org.sonar.api.internal.SonarQubeVersionFactory; 062import org.sonar.api.measures.Metric; 063import org.sonar.api.utils.System2; 064import org.sonar.api.utils.Version; 065import org.sonar.duplications.internal.pmd.TokensLine; 066 067/** 068 * Utility class to help testing {@link Sensor}. This is not an API and method signature may evolve. 069 * 070 * Usage: call {@link #create(File)} to create an "in memory" implementation of {@link SensorContext} with a filesystem initialized with provided baseDir. 071 * <p> 072 * You have to manually register inputFiles using: 073 * <pre> 074 * sensorContextTester.fileSystem().add(new DefaultInputFile("myProjectKey", "src/Foo.java") 075 .setLanguage("java") 076 .initMetadata("public class Foo {\n}")); 077 * </pre> 078 * <p> 079 * Then pass it to your {@link Sensor}. You can then query elements provided by your sensor using methods {@link #allIssues()}, ... 080 * 081 */ 082@Beta 083public class SensorContextTester implements SensorContext { 084 085 private Settings settings; 086 private DefaultFileSystem fs; 087 private ActiveRules activeRules; 088 private InMemorySensorStorage sensorStorage; 089 private InputModule module; 090 private SonarQubeVersion sqVersion; 091 092 private SensorContextTester(Path moduleBaseDir) { 093 this.settings = new Settings(); 094 this.fs = new DefaultFileSystem(moduleBaseDir); 095 this.activeRules = new ActiveRulesBuilder().build(); 096 this.sensorStorage = new InMemorySensorStorage(); 097 this.module = new DefaultInputModule("projectKey"); 098 this.sqVersion = SonarQubeVersionFactory.create(System2.INSTANCE); 099 } 100 101 public static SensorContextTester create(File moduleBaseDir) { 102 return new SensorContextTester(moduleBaseDir.toPath()); 103 } 104 105 public static SensorContextTester create(Path moduleBaseDir) { 106 return new SensorContextTester(moduleBaseDir); 107 } 108 109 @Override 110 public Settings settings() { 111 return settings; 112 } 113 114 public SensorContextTester setSettings(Settings settings) { 115 this.settings = settings; 116 return this; 117 } 118 119 @Override 120 public DefaultFileSystem fileSystem() { 121 return fs; 122 } 123 124 public SensorContextTester setFileSystem(DefaultFileSystem fs) { 125 this.fs = fs; 126 return this; 127 } 128 129 @Override 130 public ActiveRules activeRules() { 131 return activeRules; 132 } 133 134 public SensorContextTester setActiveRules(ActiveRules activeRules) { 135 this.activeRules = activeRules; 136 return this; 137 } 138 139 /** 140 * Default value is the version of this API. You can override it 141 * using {@link #setSonarQubeVersion(Version)} to test your Sensor behavior. 142 */ 143 @Override 144 public Version getSonarQubeVersion() { 145 return sqVersion.get(); 146 } 147 148 public SensorContextTester setSonarQubeVersion(Version version) { 149 this.sqVersion = new SonarQubeVersion(version); 150 return this; 151 } 152 153 @Override 154 public InputModule module() { 155 return module; 156 } 157 158 @Override 159 public <G extends Serializable> NewMeasure<G> newMeasure() { 160 return new DefaultMeasure<>(sensorStorage); 161 } 162 163 public Collection<Measure> measures(String componentKey) { 164 return sensorStorage.measuresByComponentAndMetric.row(componentKey).values(); 165 } 166 167 public <G extends Serializable> Measure<G> measure(String componetKey, Metric<G> metric) { 168 return measure(componetKey, metric.key()); 169 } 170 171 public <G extends Serializable> Measure<G> measure(String componentKey, String metricKey) { 172 return sensorStorage.measuresByComponentAndMetric.row(componentKey).get(metricKey); 173 } 174 175 @Override 176 public NewIssue newIssue() { 177 return new DefaultIssue(sensorStorage); 178 } 179 180 public Collection<Issue> allIssues() { 181 return sensorStorage.allIssues; 182 } 183 184 @CheckForNull 185 public Integer lineHits(String fileKey, CoverageType type, int line) { 186 Map<CoverageType, DefaultCoverage> defaultCoverageByType = sensorStorage.coverageByComponent.get(fileKey); 187 if (defaultCoverageByType == null) { 188 return null; 189 } 190 if (defaultCoverageByType.containsKey(type)) { 191 return defaultCoverageByType.get(type).hitsByLine().get(line); 192 } 193 return null; 194 } 195 196 @CheckForNull 197 public Integer conditions(String fileKey, CoverageType type, int line) { 198 Map<CoverageType, DefaultCoverage> defaultCoverageByType = sensorStorage.coverageByComponent.get(fileKey); 199 if (defaultCoverageByType == null) { 200 return null; 201 } 202 if (defaultCoverageByType.containsKey(type)) { 203 return defaultCoverageByType.get(type).conditionsByLine().get(line); 204 } 205 return null; 206 } 207 208 @CheckForNull 209 public Integer coveredConditions(String fileKey, CoverageType type, int line) { 210 Map<CoverageType, DefaultCoverage> defaultCoverageByType = sensorStorage.coverageByComponent.get(fileKey); 211 if (defaultCoverageByType == null) { 212 return null; 213 } 214 if (defaultCoverageByType.containsKey(type)) { 215 return defaultCoverageByType.get(type).coveredConditionsByLine().get(line); 216 } 217 return null; 218 } 219 220 @CheckForNull 221 public List<TokensLine> cpdTokens(String componentKey) { 222 DefaultCpdTokens defaultCpdTokens = sensorStorage.cpdTokensByComponent.get(componentKey); 223 return defaultCpdTokens != null ? defaultCpdTokens.getTokenLines() : null; 224 } 225 226 @Override 227 public NewHighlighting newHighlighting() { 228 return new DefaultHighlighting(sensorStorage); 229 } 230 231 @Override 232 public NewCoverage newCoverage() { 233 return new DefaultCoverage(sensorStorage); 234 } 235 236 @Override 237 public NewCpdTokens newCpdTokens() { 238 return new DefaultCpdTokens(settings, sensorStorage); 239 } 240 241 @Override 242 public NewSymbolTable newSymbolTable() { 243 return new DefaultSymbolTable(sensorStorage); 244 } 245 246 /** 247 * Return list of syntax highlighting applied for a given position in a file. The result is a list because in theory you 248 * can apply several styles to the same range. 249 * @param componentKey Key of the file like 'myProjectKey:src/foo.php' 250 * @param line Line you want to query 251 * @param lineOffset Offset you want to query. 252 * @return List of styles applied to this position or empty list if there is no highlighting at this position. 253 */ 254 public List<TypeOfText> highlightingTypeAt(String componentKey, int line, int lineOffset) { 255 DefaultHighlighting syntaxHighlightingData = sensorStorage.highlightingByComponent.get(componentKey); 256 if (syntaxHighlightingData == null) { 257 return Collections.emptyList(); 258 } 259 List<TypeOfText> result = new ArrayList<>(); 260 DefaultTextPointer location = new DefaultTextPointer(line, lineOffset); 261 for (SyntaxHighlightingRule sortedRule : syntaxHighlightingData.getSyntaxHighlightingRuleSet()) { 262 if (sortedRule.range().start().compareTo(location) <= 0 && sortedRule.range().end().compareTo(location) > 0) { 263 result.add(sortedRule.getTextType()); 264 } 265 } 266 return result; 267 } 268 269 /** 270 * Return list of symbol references ranges for the symbol at a given position in a file. 271 * @param componentKey Key of the file like 'myProjectKey:src/foo.php' 272 * @param line Line you want to query 273 * @param lineOffset Offset you want to query. 274 * @return List of references for the symbol or empty list if there is no symbol at this position or if there is no reference for this symbol. 275 */ 276 public Collection<TextRange> referencesForSymbolAt(String componentKey, int line, int lineOffset) { 277 DefaultSymbolTable symbolTable = sensorStorage.symbolsPerComponent.get(componentKey); 278 if (symbolTable == null) { 279 return Collections.emptyList(); 280 } 281 DefaultTextPointer location = new DefaultTextPointer(line, lineOffset); 282 for (Map.Entry<TextRange, Set<TextRange>> symbol : symbolTable.getReferencesBySymbol().entrySet()) { 283 if (symbol.getKey().start().compareTo(location) <= 0 && symbol.getKey().end().compareTo(location) > 0) { 284 return symbol.getValue(); 285 } 286 } 287 return Collections.emptyList(); 288 } 289 290}