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}