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