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