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