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