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