001/*-------------------------------------------------------------------------+ 002| | 003| Copyright (c) 2009-2018 CQSE GmbH | 004| | 005+-------------------------------------------------------------------------*/ 006package eu.cqse.check.framework.checktest; 007 008import static java.util.Objects.requireNonNull; 009import static org.assertj.core.api.Assertions.assertThat; 010 011import java.util.ArrayList; 012import java.util.Collections; 013import java.util.EnumSet; 014import java.util.HashMap; 015import java.util.List; 016import java.util.Map; 017import java.util.regex.Matcher; 018import java.util.regex.Pattern; 019 020import org.conqat.engine.core.core.ConQATException; 021import org.conqat.lib.commons.assertion.CCSMAssert; 022import org.conqat.lib.commons.collections.CollectionUtils; 023import org.conqat.lib.commons.collections.Pair; 024import org.conqat.lib.commons.filesystem.FileSystemUtils; 025import org.conqat.lib.commons.resources.Resource; 026import org.conqat.lib.commons.string.StringUtils; 027 028import eu.cqse.check.framework.core.CheckException; 029import eu.cqse.check.framework.core.phase.ECodeViewOption; 030import eu.cqse.check.framework.core.phase.ECodeViewOption.ETextViewOption; 031import eu.cqse.check.framework.core.phase.ITokenElementContext; 032import eu.cqse.check.framework.core.xpath.DocumentRootShallowEntity; 033import eu.cqse.check.framework.preprocessor.c.CPreprocessor; 034import eu.cqse.check.framework.scanner.ELanguage; 035import eu.cqse.check.framework.scanner.IToken; 036import eu.cqse.check.framework.scanner.ScannerUtils; 037import eu.cqse.check.framework.shallowparser.ShallowParserException; 038import eu.cqse.check.framework.shallowparser.ShallowParserFactory; 039import eu.cqse.check.framework.shallowparser.framework.ShallowEntity; 040import eu.cqse.check.framework.typetracker.ITypeResolution; 041import eu.cqse.check.framework.typetracker.TypeTrackerFactory; 042import eu.cqse.check.util.clang.ClangTranslationUnitWrapper; 043 044/** 045 * The context implementation used during testing. 046 * 047 * This context can maintain filtered and unfiltered code separate from each 048 * other. However, if the filtered and unfiltered content is different, the line 049 * numbers of created findings might not be correct because we cannot convert 050 * between filtered and unfiltered offsets like normal teamscale. 051 * 052 * Therefore, line numbers for reported findings might be wrong in the test 053 * context depending on which code (filtered or unfiltered) the custom check 054 * computes its finding offsets. 055 */ 056public class TestTokenElementContext implements ITokenElementContext { 057 058 /** The underlying code's path. */ 059 private final String uniformPath; 060 061 /** The underlying, unfiltered code. */ 062 private final String unfilteredCode; 063 064 /** The underlying, filtered code. */ 065 private final String filteredCode; 066 067 private final ELanguage language; 068 069 /** 070 * Filtered code offsets. Each pairs of offsets denotes the start (inclusive) 071 * and end (exclusive) offset of a section of filtered code. Offsets refer to 072 * {@link #unfilteredCode}. 073 * 074 * Sorted by Pair::getFirst. Regions are non-overlapping. 075 */ 076 private final List<Pair<Integer, Integer>> filteredRegions; 077 078 private final Map<ECodeViewOption, List<IToken>> tokensCache = new HashMap<>(); 079 private final Map<ECodeViewOption, ShallowEntity> rootEntityCache = new HashMap<>(); 080 private final Map<ECodeViewOption, List<ShallowEntity>> abstractSyntaxTreeCache = new HashMap<>(); 081 082 private final Map<ECodeViewOption, ITypeResolution> typeResolutionCache = new HashMap<>(); 083 084 public TestTokenElementContext(Resource codeFile, ELanguage language) { 085 this(codeFile.getPath(), language, codeFile.getContent(), Collections.emptyList()); 086 } 087 088 public TestTokenElementContext(String uniformPath, ELanguage language, String unfilteredCode, 089 List<Pattern> filterPatterns) { 090 this.uniformPath = uniformPath; 091 this.unfilteredCode = unfilteredCode; 092 this.language = requireNonNull(language); 093 filteredRegions = new ArrayList<>(); 094 for (Pattern filterPattern : filterPatterns) { 095 Matcher matcher = filterPattern.matcher(unfilteredCode); 096 while (matcher.find()) { 097 filteredRegions.add(new Pair<>(matcher.start(), matcher.end())); 098 } 099 } 100 filteredRegions.sort(CollectionUtils.getPairComparator()); 101 assertFilterRegionsValid(filteredRegions); 102 filteredCode = filterContent(unfilteredCode, filteredRegions); 103 } 104 105 /** 106 * filterRegions. Returns the filtered code. Filters character sequences from 107 * the given unfilteredCode based on the 108 * 109 * Assumes that the filter regions are sorted, non-empty and non-overlapping. 110 */ 111 private static String filterContent(String unfilteredCode, List<Pair<Integer, Integer>> filterRegions) { 112 if (filterRegions.isEmpty()) { 113 return unfilteredCode; 114 } 115 StringBuilder filteredCode = new StringBuilder(); 116 if (filterRegions.get(0).getFirst() != 0) { 117 filteredCode.append(unfilteredCode.subSequence(0, filterRegions.get(0).getFirst())); 118 } 119 for (int i = 0; i < filterRegions.size() - 1; i++) { 120 filteredCode.append( 121 unfilteredCode.subSequence(filterRegions.get(i).getSecond(), filterRegions.get(i + 1).getFirst())); 122 } 123 if (CollectionUtils.getLast(filterRegions).getSecond() != unfilteredCode.length()) { 124 125 filteredCode.append(unfilteredCode.subSequence(CollectionUtils.getLast(filterRegions).getSecond(), 126 unfilteredCode.length())); 127 } 128 return filteredCode.toString(); 129 } 130 131 /** 132 * Asserts that the {@link #filteredRegions} are valid (each region is non-empty 133 * and regions do not overlap). 134 */ 135 private static void assertFilterRegionsValid(List<Pair<Integer, Integer>> filteredRegions) { 136 filteredRegions.forEach(region -> assertThat(region.getFirst() < region.getSecond()).isTrue()); 137 for (int i = 0; i < filteredRegions.size() - 1; i++) { 138 assertThat(filteredRegions.get(i).getSecond() <= filteredRegions.get(i + 1).getFirst()) 139 .as("Overlapping filter regions.").isTrue(); 140 } 141 } 142 143 /** {@inheritDoc} */ 144 @Override 145 public ELanguage getLanguage() { 146 147 return language; 148 } 149 150 /** {@inheritDoc} */ 151 @Override 152 public String getUniformPath() { 153 return uniformPath; 154 } 155 156 /** Template method that is called before accessing the AST. */ 157 protected void accessAst() { 158 // does nothing 159 } 160 161 /** Template method called before accessing type resolution. */ 162 protected void accessTypeResolution() { 163 // does nothing 164 } 165 166 /** 167 * Returns the offset in filtered code that represents the same character as the 168 * given offset in the unfiltered Code. 169 */ 170 protected int getFilteredOffset(int unfilteredCodeOffset) { 171 int filteredOffset = unfilteredCodeOffset; 172 for (Pair<Integer, Integer> filterRegion : filteredRegions) { 173 if (filterRegion.getFirst() < unfilteredCodeOffset && filterRegion.getSecond() <= unfilteredCodeOffset) { 174 // unfilteredCodeOffset is after this filtered region. 175 // Subtract size of filtered region 176 filteredOffset -= (filterRegion.getSecond() - filterRegion.getFirst()); 177 } else if (filterRegion.getFirst() < unfilteredCodeOffset) { 178 // unfilteredCodeOffset is in this filtered region. 179 // Return the first offset after this region and skip the 180 // assertion. 181 return getFilteredOffset(filterRegion.getSecond()); 182 } else { 183 // unfilteredCodeOffset is before this region 184 break; 185 } 186 } 187 CCSMAssert.isTrue(filteredCode.codePointAt(filteredOffset) == unfilteredCode.codePointAt(unfilteredCodeOffset), 188 "Computation of filtered offset in Custom check test context is wrong."); 189 return filteredOffset; 190 } 191 192 /** 193 * Returns whether the given offset in unfiltered code is in a filtered region. 194 */ 195 protected boolean isInFilteredRegion(int unfilteredCodeOffset) { 196 for (Pair<Integer, Integer> filterRegion : filteredRegions) { 197 if (filterRegion.getFirst() <= unfilteredCodeOffset && unfilteredCodeOffset < filterRegion.getSecond()) { 198 return true; 199 } 200 } 201 return false; 202 } 203 204 /** {@inheritDoc} */ 205 @Override 206 public String getTextContent(ETextViewOption view) { 207 switch (view) { 208 case UNFILTERED_CONTENT: 209 return unfilteredCode; 210 case FILTERED_CONTENT: 211 return filteredCode; 212 default: 213 throw new AssertionError("Missing case for " + view); 214 } 215 } 216 217 /** {@inheritDoc} */ 218 @Override 219 public List<IToken> getTokens(ECodeViewOption view) { 220 if (tokensCache.containsKey(view)) { 221 return tokensCache.get(view); 222 } 223 List<IToken> tokens = computeTokens(view); 224 tokensCache.put(view, tokens); 225 return tokens; 226 } 227 228 /** 229 * Computes a new {@link IToken} list for the given {@link ECodeViewOption}. 230 * Throws an {@link AssertionError} if the given {@link ECodeViewOption} is not 231 * supported. 232 */ 233 private List<IToken> computeTokens(ECodeViewOption view) throws AssertionError { 234 switch (view) { 235 case UNFILTERED: 236 return CollectionUtils.asUnmodifiable(ScannerUtils 237 .getTokens(getTextContent(ETextViewOption.UNFILTERED_CONTENT), getLanguage(), getUniformPath())); 238 case FILTERED: 239 return CollectionUtils.asUnmodifiable(ScannerUtils 240 .getTokens(getTextContent(ETextViewOption.FILTERED_CONTENT), getLanguage(), getUniformPath())); 241 case UNFILTERED_PREPROCESSED: 242 return calculateLocalPreprocessedTokens(ScannerUtils 243 .getTokens(getTextContent(ETextViewOption.UNFILTERED_CONTENT), getLanguage(), getUniformPath())); 244 case FILTERED_PREPROCESSED: 245 return calculateLocalPreprocessedTokens(ScannerUtils 246 .getTokens(getTextContent(ETextViewOption.FILTERED_CONTENT), getLanguage(), getUniformPath())); 247 default: 248 throw new AssertionError("Missing case for " + view); 249 } 250 } 251 252 /** {@inheritDoc} */ 253 @Override 254 public ShallowEntity getRootEntity(ECodeViewOption view) throws CheckException { 255 if (rootEntityCache.containsKey(view)) { 256 return rootEntityCache.get(view); 257 } 258 ShallowEntity rootEntity = computeRootEntity(view); 259 rootEntityCache.put(view, rootEntity); 260 return rootEntity; 261 } 262 263 /** 264 * Computes a new root entity for the given {@link ECodeViewOption}. Throws an 265 * {@link AssertionError} if the given {@link ECodeViewOption} is not supported. 266 */ 267 private ShallowEntity computeRootEntity(ECodeViewOption view) throws CheckException, AssertionError { 268 /* 269 * This might violate an assumption that each ShallowEntity is only part of one 270 * AST (after this method it is part of the original AST and of the "rooted" 271 * tree). However, this is necessary as some checks (e.g., 272 * AvoidUsingSystemThreadingThreadAbort) rely on the fact that the same (Object 273 * equality) Entities are returned by this method and used to construct the 274 * ITypeResolution. 275 */ 276 DocumentRootShallowEntity rootEntity = new DocumentRootShallowEntity(getElementName()); 277 switch (view) { 278 case UNFILTERED_PREPROCESSED: 279 rootEntity.setChildren(getAbstractSyntaxTree(ECodeViewOption.UNFILTERED_PREPROCESSED)); 280 break; 281 case FILTERED_PREPROCESSED: 282 rootEntity.setChildren(getAbstractSyntaxTree(ECodeViewOption.FILTERED_PREPROCESSED)); 283 break; 284 case FILTERED: 285 rootEntity.setChildren(getAbstractSyntaxTree(ECodeViewOption.FILTERED)); 286 break; 287 default: 288 throw new AssertionError("The ECodeViewOption " + view + " is not supported."); 289 } 290 return rootEntity; 291 } 292 293 /** {@inheritDoc} */ 294 @Override 295 public List<ShallowEntity> getAbstractSyntaxTree(ECodeViewOption view) throws CheckException { 296 if (abstractSyntaxTreeCache.containsKey(view)) { 297 return abstractSyntaxTreeCache.get(view); 298 } 299 List<ShallowEntity> typeResolution = computeAbstractSyntaxTree(view); 300 abstractSyntaxTreeCache.put(view, typeResolution); 301 return typeResolution; 302 } 303 304 /** 305 * Computes a new Abstract syntax Tree for the given {@link ECodeViewOption}. 306 * Throws an {@link AssertionError} if the given {@link ECodeViewOption} is not 307 * supported. 308 */ 309 private List<ShallowEntity> computeAbstractSyntaxTree(ECodeViewOption view) throws CheckException, AssertionError { 310 switch (view) { 311 case UNFILTERED_PREPROCESSED: 312 try { 313 return parseTokens(ECodeViewOption.UNFILTERED_PREPROCESSED, false); 314 } catch (ShallowParserException e) { 315 throw new CheckException(e); 316 } 317 case FILTERED: 318 try { 319 return parseTokens(ECodeViewOption.FILTERED_PREPROCESSED, true); 320 } catch (ShallowParserException e) { 321 throw new CheckException(e); 322 } 323 case FILTERED_PREPROCESSED: 324 try { 325 return parseTokens(ECodeViewOption.FILTERED_PREPROCESSED, false); 326 } catch (ShallowParserException e) { 327 throw new CheckException(e); 328 } 329 default: 330 throw new AssertionError("The ECodeViewOption " + view + " is not supported."); 331 } 332 } 333 334 /** {@inheritDoc} */ 335 @Override 336 public ITypeResolution getTypeResolution(ECodeViewOption view) throws CheckException { 337 if (typeResolutionCache.containsKey(view)) { 338 return typeResolutionCache.get(view); 339 } 340 ITypeResolution typeResolution = computeTypeResolution(view); 341 typeResolutionCache.put(view, typeResolution); 342 return typeResolution; 343 } 344 345 /** 346 * Computes a new {@link ITypeResolution} for the given {@link ECodeViewOption}. 347 * Throws an {@link AssertionError} if the given {@link ECodeViewOption} is not 348 * supported. 349 */ 350 private ITypeResolution computeTypeResolution(ECodeViewOption view) throws CheckException { 351 switch (view) { 352 case UNFILTERED_PREPROCESSED: 353 accessTypeResolution(); 354 return TypeTrackerFactory.createTypeTracker(getLanguage()) 355 .createTypeResolution(getAbstractSyntaxTree(ECodeViewOption.UNFILTERED_PREPROCESSED)); 356 case FILTERED: 357 accessTypeResolution(); 358 return TypeTrackerFactory.createTypeTracker(getLanguage()) 359 .createTypeResolution(getAbstractSyntaxTree(ECodeViewOption.FILTERED)); 360 case FILTERED_PREPROCESSED: 361 accessTypeResolution(); 362 return TypeTrackerFactory.createTypeTracker(getLanguage()) 363 .createTypeResolution(getAbstractSyntaxTree(ECodeViewOption.FILTERED_PREPROCESSED)); 364 default: 365 throw new AssertionError("The ECodeViewOption " + view + " is not supported."); 366 } 367 } 368 369 /** Returns the file name of the current uniform path. */ 370 private String getElementName() { 371 return StringUtils.getLastPart(getUniformPath(), FileSystemUtils.UNIX_SEPARATOR); 372 } 373 374 /** 375 * Parses the given Tokens and returns the shallow entities. 376 */ 377 private List<ShallowEntity> parseTokens(ECodeViewOption codeViewOption, boolean removePreprocessorTokens) 378 throws ShallowParserException { 379 CCSMAssert.isTrue(EnumSet.of(ECodeViewOption.FILTERED_PREPROCESSED, ECodeViewOption.UNFILTERED_PREPROCESSED) 380 .contains(codeViewOption), "can parse preprocessed tokens only: " + codeViewOption); 381 List<ShallowEntity> entities = ShallowParserFactory.createParser(getLanguage()) 382 .parseTopLevel(getTokens(codeViewOption)); 383 if (getLanguage() == ELanguage.CPP && removePreprocessorTokens) { 384 CPreprocessor.removePreprocessorTokens(entities); 385 } 386 return entities; 387 } 388 389 @Override 390 public ClangTranslationUnitWrapper getClangTranslationUnitWrapper() { 391 try { 392 return ClangTranslationUnitWrapper.createForContext(this); 393 } catch (ConQATException e) { 394 CCSMAssert.fail("Exception while parsing with clang: " + e.getMessage(), e); 395 return null; 396 } 397 } 398}