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