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}