001/* 002 * SonarQube 003 * Copyright (C) 2009-2017 SonarSource SA 004 * mailto:info 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.collect.ImmutableMap; 023 024import java.io.File; 025import java.io.Serializable; 026import java.nio.file.Path; 027import java.util.ArrayList; 028import java.util.Collection; 029import java.util.Collections; 030import java.util.List; 031import java.util.Map; 032import java.util.Objects; 033import java.util.Set; 034import java.util.stream.Stream; 035import javax.annotation.CheckForNull; 036import javax.annotation.Nullable; 037import org.sonar.api.SonarQubeSide; 038import org.sonar.api.SonarRuntime; 039import org.sonar.api.batch.fs.InputFile; 040import org.sonar.api.batch.fs.InputModule; 041import org.sonar.api.batch.fs.TextRange; 042import org.sonar.api.batch.fs.internal.DefaultFileSystem; 043import org.sonar.api.batch.fs.internal.DefaultInputFile; 044import org.sonar.api.batch.fs.internal.DefaultInputModule; 045import org.sonar.api.batch.fs.internal.DefaultTextPointer; 046import org.sonar.api.batch.rule.ActiveRules; 047import org.sonar.api.batch.rule.internal.ActiveRulesBuilder; 048import org.sonar.api.batch.sensor.Sensor; 049import org.sonar.api.batch.sensor.SensorContext; 050import org.sonar.api.batch.sensor.coverage.NewCoverage; 051import org.sonar.api.batch.sensor.coverage.internal.DefaultCoverage; 052import org.sonar.api.batch.sensor.cpd.NewCpdTokens; 053import org.sonar.api.batch.sensor.cpd.internal.DefaultCpdTokens; 054import org.sonar.api.batch.sensor.error.AnalysisError; 055import org.sonar.api.batch.sensor.error.NewAnalysisError; 056import org.sonar.api.batch.sensor.error.internal.DefaultAnalysisError; 057import org.sonar.api.batch.sensor.highlighting.NewHighlighting; 058import org.sonar.api.batch.sensor.highlighting.TypeOfText; 059import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting; 060import org.sonar.api.batch.sensor.highlighting.internal.SyntaxHighlightingRule; 061import org.sonar.api.batch.sensor.issue.Issue; 062import org.sonar.api.batch.sensor.issue.NewIssue; 063import org.sonar.api.batch.sensor.issue.internal.DefaultIssue; 064import org.sonar.api.batch.sensor.measure.Measure; 065import org.sonar.api.batch.sensor.measure.NewMeasure; 066import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure; 067import org.sonar.api.batch.sensor.symbol.NewSymbolTable; 068import org.sonar.api.batch.sensor.symbol.internal.DefaultSymbolTable; 069import org.sonar.api.config.MapSettings; 070import org.sonar.api.config.Settings; 071import org.sonar.api.internal.ApiVersion; 072import org.sonar.api.internal.SonarRuntimeImpl; 073import org.sonar.api.measures.Metric; 074import org.sonar.api.utils.System2; 075import org.sonar.api.utils.Version; 076import org.sonar.duplications.internal.pmd.TokensLine; 077 078/** 079 * Utility class to help testing {@link Sensor}. This is not an API and method signature may evolve. 080 * 081 * Usage: call {@link #create(File)} to create an "in memory" implementation of {@link SensorContext} with a filesystem initialized with provided baseDir. 082 * <p> 083 * You have to manually register inputFiles using: 084 * <pre> 085 * sensorContextTester.fileSystem().add(new DefaultInputFile("myProjectKey", "src/Foo.java") 086 .setLanguage("java") 087 .initMetadata("public class Foo {\n}")); 088 * </pre> 089 * <p> 090 * Then pass it to your {@link Sensor}. You can then query elements provided by your sensor using methods {@link #allIssues()}, ... 091 * 092 */ 093public class SensorContextTester implements SensorContext { 094 095 private Settings settings; 096 private DefaultFileSystem fs; 097 private ActiveRules activeRules; 098 private InMemorySensorStorage sensorStorage; 099 private InputModule module; 100 private SonarRuntime runtime; 101 private boolean cancelled; 102 103 private SensorContextTester(Path moduleBaseDir) { 104 this.settings = new MapSettings(); 105 this.fs = new DefaultFileSystem(moduleBaseDir); 106 this.activeRules = new ActiveRulesBuilder().build(); 107 this.sensorStorage = new InMemorySensorStorage(); 108 this.module = new DefaultInputModule("projectKey"); 109 this.runtime = SonarRuntimeImpl.forSonarQube(ApiVersion.load(System2.INSTANCE), SonarQubeSide.SCANNER); 110 } 111 112 public static SensorContextTester create(File moduleBaseDir) { 113 return new SensorContextTester(moduleBaseDir.toPath()); 114 } 115 116 public static SensorContextTester create(Path moduleBaseDir) { 117 return new SensorContextTester(moduleBaseDir); 118 } 119 120 @Override 121 public Settings settings() { 122 return settings; 123 } 124 125 public SensorContextTester setSettings(Settings settings) { 126 this.settings = settings; 127 return this; 128 } 129 130 @Override 131 public DefaultFileSystem fileSystem() { 132 return fs; 133 } 134 135 public SensorContextTester setFileSystem(DefaultFileSystem fs) { 136 this.fs = fs; 137 return this; 138 } 139 140 @Override 141 public ActiveRules activeRules() { 142 return activeRules; 143 } 144 145 public SensorContextTester setActiveRules(ActiveRules activeRules) { 146 this.activeRules = activeRules; 147 return this; 148 } 149 150 /** 151 * Default value is the version of this API at compilation time. You can override it 152 * using {@link #setRuntime(SonarRuntime)} to test your Sensor behaviour. 153 */ 154 @Override 155 public Version getSonarQubeVersion() { 156 return runtime().getApiVersion(); 157 } 158 159 /** 160 * @see #setRuntime(SonarRuntime) to override defaults (SonarQube scanner with version 161 * of this API as used at compilation time). 162 */ 163 @Override 164 public SonarRuntime runtime() { 165 return runtime; 166 } 167 168 public SensorContextTester setRuntime(SonarRuntime runtime) { 169 this.runtime = runtime; 170 return this; 171 } 172 173 @Override 174 public boolean isCancelled() { 175 return cancelled; 176 } 177 178 public void setCancelled(boolean cancelled) { 179 this.cancelled = cancelled; 180 } 181 182 @Override 183 public InputModule module() { 184 return module; 185 } 186 187 @Override 188 public <G extends Serializable> NewMeasure<G> newMeasure() { 189 return new DefaultMeasure<>(sensorStorage); 190 } 191 192 public Collection<Measure> measures(String componentKey) { 193 return sensorStorage.measuresByComponentAndMetric.row(componentKey).values(); 194 } 195 196 public <G extends Serializable> Measure<G> measure(String componentKey, Metric<G> metric) { 197 return measure(componentKey, metric.key()); 198 } 199 200 public <G extends Serializable> Measure<G> measure(String componentKey, String metricKey) { 201 return sensorStorage.measuresByComponentAndMetric.row(componentKey).get(metricKey); 202 } 203 204 @Override 205 public NewIssue newIssue() { 206 return new DefaultIssue(sensorStorage); 207 } 208 209 public Collection<Issue> allIssues() { 210 return sensorStorage.allIssues; 211 } 212 213 public Collection<AnalysisError> allAnalysisErrors() { 214 return sensorStorage.allAnalysisErrors; 215 } 216 217 @CheckForNull 218 public Integer lineHits(String fileKey, int line) { 219 return sensorStorage.coverageByComponent.get(fileKey).stream() 220 .map(c -> c.hitsByLine().get(line)) 221 .flatMap(Stream::of) 222 .filter(Objects::nonNull) 223 .reduce(null, SensorContextTester::sumOrNull); 224 } 225 226 @CheckForNull 227 public static Integer sumOrNull(@Nullable Integer o1, @Nullable Integer o2) { 228 return o1 == null ? o2 : (o1 + o2); 229 } 230 231 @CheckForNull 232 public Integer conditions(String fileKey, int line) { 233 return sensorStorage.coverageByComponent.get(fileKey).stream() 234 .map(c -> c.conditionsByLine().get(line)) 235 .flatMap(Stream::of) 236 .filter(Objects::nonNull) 237 .reduce(null, SensorContextTester::maxOrNull); 238 } 239 240 @CheckForNull 241 public Integer coveredConditions(String fileKey, int line) { 242 return sensorStorage.coverageByComponent.get(fileKey).stream() 243 .map(c -> c.coveredConditionsByLine().get(line)) 244 .flatMap(Stream::of) 245 .filter(Objects::nonNull) 246 .reduce(null, SensorContextTester::maxOrNull); 247 } 248 249 @CheckForNull 250 public static Integer maxOrNull(@Nullable Integer o1, @Nullable Integer o2) { 251 return o1 == null ? o2 : Math.max(o1, o2); 252 } 253 254 @CheckForNull 255 public List<TokensLine> cpdTokens(String componentKey) { 256 DefaultCpdTokens defaultCpdTokens = sensorStorage.cpdTokensByComponent.get(componentKey); 257 return defaultCpdTokens != null ? defaultCpdTokens.getTokenLines() : null; 258 } 259 260 @Override 261 public NewHighlighting newHighlighting() { 262 return new DefaultHighlighting(sensorStorage); 263 } 264 265 @Override 266 public NewCoverage newCoverage() { 267 return new DefaultCoverage(sensorStorage); 268 } 269 270 @Override 271 public NewCpdTokens newCpdTokens() { 272 return new DefaultCpdTokens(settings, sensorStorage); 273 } 274 275 @Override 276 public NewSymbolTable newSymbolTable() { 277 return new DefaultSymbolTable(sensorStorage); 278 } 279 280 @Override 281 public NewAnalysisError newAnalysisError() { 282 return new DefaultAnalysisError(sensorStorage); 283 } 284 285 /** 286 * Return list of syntax highlighting applied for a given position in a file. The result is a list because in theory you 287 * can apply several styles to the same range. 288 * @param componentKey Key of the file like 'myProjectKey:src/foo.php' 289 * @param line Line you want to query 290 * @param lineOffset Offset you want to query. 291 * @return List of styles applied to this position or empty list if there is no highlighting at this position. 292 */ 293 public List<TypeOfText> highlightingTypeAt(String componentKey, int line, int lineOffset) { 294 DefaultHighlighting syntaxHighlightingData = sensorStorage.highlightingByComponent.get(componentKey); 295 if (syntaxHighlightingData == null) { 296 return Collections.emptyList(); 297 } 298 List<TypeOfText> result = new ArrayList<>(); 299 DefaultTextPointer location = new DefaultTextPointer(line, lineOffset); 300 for (SyntaxHighlightingRule sortedRule : syntaxHighlightingData.getSyntaxHighlightingRuleSet()) { 301 if (sortedRule.range().start().compareTo(location) <= 0 && sortedRule.range().end().compareTo(location) > 0) { 302 result.add(sortedRule.getTextType()); 303 } 304 } 305 return result; 306 } 307 308 /** 309 * Return list of symbol references ranges for the symbol at a given position in a file. 310 * @param componentKey Key of the file like 'myProjectKey:src/foo.php' 311 * @param line Line you want to query 312 * @param lineOffset Offset you want to query. 313 * @return List of references for the symbol (potentially empty) or null if there is no symbol at this position. 314 */ 315 @CheckForNull 316 public Collection<TextRange> referencesForSymbolAt(String componentKey, int line, int lineOffset) { 317 DefaultSymbolTable symbolTable = sensorStorage.symbolsPerComponent.get(componentKey); 318 if (symbolTable == null) { 319 return null; 320 } 321 DefaultTextPointer location = new DefaultTextPointer(line, lineOffset); 322 for (Map.Entry<TextRange, Set<TextRange>> symbol : symbolTable.getReferencesBySymbol().entrySet()) { 323 if (symbol.getKey().start().compareTo(location) <= 0 && symbol.getKey().end().compareTo(location) > 0) { 324 return symbol.getValue(); 325 } 326 } 327 return null; 328 } 329 330 @Override 331 public void addContextProperty(String key, String value) { 332 sensorStorage.storeProperty(key, value); 333 } 334 335 /** 336 * @return an immutable map of the context properties defined with {@link SensorContext#addContextProperty(String, String)}. 337 * @since 6.1 338 */ 339 public Map<String, String> getContextProperties() { 340 return ImmutableMap.copyOf(sensorStorage.contextProperties); 341 } 342 343 @Override 344 public void markForPublishing(InputFile inputFile) { 345 DefaultInputFile file = (DefaultInputFile) inputFile; 346 file.setPublish(true); 347 } 348}