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