001/*-------------------------------------------------------------------------+ 002| | 003| Copyright 2005-2011 the ConQAT Project | 004| | 005| Licensed under the Apache License, Version 2.0 (the "License"); | 006| you may not use this file except in compliance with the License. | 007| You may obtain a copy of the License at | 008| | 009| http://www.apache.org/licenses/LICENSE-2.0 | 010| | 011| Unless required by applicable law or agreed to in writing, software | 012| distributed under the License is distributed on an "AS IS" BASIS, | 013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 014| See the License for the specific language governing permissions and | 015| limitations under the License. | 016+-------------------------------------------------------------------------*/ 017package eu.cqse.check.framework.checktest; 018 019import static org.assertj.core.api.Assertions.assertThat; 020 021import java.io.File; 022import java.io.IOException; 023import java.io.Serializable; 024import java.util.ArrayList; 025import java.util.Collections; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.function.Function; 030import java.util.regex.Pattern; 031 032import org.conqat.lib.commons.assertion.CCSMAssert; 033import org.conqat.lib.commons.filesystem.FileSystemUtils; 034import org.conqat.lib.commons.region.OffsetBasedRegion; 035import org.conqat.lib.commons.string.LineOffsetConverter; 036import org.conqat.lib.commons.string.StringUtils; 037 038import eu.cqse.check.CheckTextRegionLocation; 039import eu.cqse.check.framework.core.CheckInfo; 040import eu.cqse.check.framework.core.ECheckParameter; 041import eu.cqse.check.framework.core.ICheckContext; 042import eu.cqse.check.framework.core.phase.ECodeViewOption.ETextViewOption; 043import eu.cqse.check.framework.core.phase.IExtractedValue; 044import eu.cqse.check.framework.core.phase.IGlobalExtractionPhase; 045 046/** 047 * The context implementation used during testing. 048 * 049 * This context can maintain filtered and unfiltered code separate from each 050 * other. However, if the filtered and unfiltered content is different, the line 051 * numbers of created findings might not be correct because we cannot convert 052 * between filtered and unfiltered offsets like normal teamscale. 053 * 054 * Therefore, line numbers for reported findings might be wrong in the test 055 * context depending on which code (filtered or unfiltered) the custom check 056 * computes its finding offsets. 057 */ 058public class TestCheckContext extends TestTokenElementContext implements ICheckContext { 059 060 /** The check info used for consistency checks. */ 061 private final CheckInfo checkInfo; 062 063 /** String representation of the findings reported. */ 064 private final StringBuilder findingsStringBuilder = new StringBuilder(); 065 066 /** Line offset converter for the file (lazy loaded). */ 067 private LineOffsetConverter unfilteredCodeLineOffsetConverter; 068 069 /** Line offset converter for the filteredCode (lazy loaded). */ 070 private LineOffsetConverter filteredCodeLineOffsetConverter; 071 072 /** Stores for each phase the results by uniform path. */ 073 private final Map<Class<?>, Map<String, List<IExtractedValue<?>>>> phaseResults = new HashMap<>(); 074 075 /** Stores for each phase the inverted results, i.e. access by value. */ 076 private final Map<Class<?>, Map<String, List<IExtractedValue<?>>>> invertedPhaseResults = new HashMap<>(); 077 078 /** Constructor. */ 079 public TestCheckContext(File codeFile, CheckInfo checkInfo) throws IOException { 080 this(FileSystemUtils.normalizeSeparators(codeFile.getAbsolutePath()), 081 StringUtils.replaceLineBreaks(FileSystemUtils.readFile(codeFile), "\n"), checkInfo); 082 } 083 084 public TestCheckContext(String uniformPath, String code, CheckInfo checkInfo) { 085 this(uniformPath, code, Collections.emptyList(), checkInfo); 086 } 087 088 public TestCheckContext(String uniformPath, String unfilteredCode, List<Pattern> filterPatterns, 089 CheckInfo checkInfo) { 090 super(uniformPath, CheckTestBase.determineLanguage(checkInfo, uniformPath), unfilteredCode, filterPatterns); 091 this.checkInfo = checkInfo; 092 assertThat(checkInfo).as("checkInfo missing for " + uniformPath).isNotNull(); 093 assertThat(checkInfo.getSupportedLanguages()) 094 .as("Check does not support language of test code file: " + uniformPath).contains(getLanguage()); 095 } 096 097 @Override 098 public void createFindingForElement(String message) { 099 findingsStringBuilder.append("Element-level: " + message + StringUtils.LINE_SEPARATOR); 100 } 101 102 /** Returns a string representation of the findings found. */ 103 public String getFindingsString() { 104 return findingsStringBuilder.toString(); 105 } 106 107 /** 108 * @see #findingsStringBuilder 109 */ 110 public StringBuilder getFindings() { 111 return findingsStringBuilder; 112 } 113 114 /** @see #checkInfo */ 115 public CheckInfo getCheckInfo() { 116 return checkInfo; 117 } 118 119 @Override 120 public void createFindingForLines(String message, int startLine, int endLine, ETextViewOption view) { 121 int filteredStartLine = startLine; 122 int filteredEndLine = endLine; 123 if (view == ETextViewOption.UNFILTERED_CONTENT) { 124 int startLineBeginOffset = getFilteredOffset(unfilteredCodeLineOffsetConverter.getOffset(startLine)); 125 int endLineEndOffset = getFilteredOffset(unfilteredCodeLineOffsetConverter.getOffset(endLine + 1) - 1); 126 filteredStartLine = filteredCodeLineOffsetConverter.getLine(startLineBeginOffset); 127 filteredEndLine = filteredCodeLineOffsetConverter.getLine(endLineEndOffset); 128 } 129 findingsStringBuilder.append( 130 "Lines " + filteredStartLine + "-" + filteredEndLine + ": " + message + StringUtils.LINE_SEPARATOR); 131 } 132 133 @Override 134 public void createFindingForOffsets(String message, int startOffset, int endOffset, ETextViewOption view) { 135 if (isInFilteredRegion(startOffset) || isInFilteredRegion(endOffset)) { 136 return; // ignore findings in filtered regions 137 } 138 int filteredStartOffset = startOffset; 139 int filteredEndOffset = endOffset; 140 if (view == ETextViewOption.UNFILTERED_CONTENT) { 141 filteredStartOffset = getFilteredOffset(startOffset); 142 filteredEndOffset = getFilteredOffset(endOffset); 143 } 144 145 if (unfilteredCodeLineOffsetConverter == null) { 146 unfilteredCodeLineOffsetConverter = new LineOffsetConverter( 147 getTextContent(ETextViewOption.FILTERED_CONTENT)); 148 } 149 150 int startLine = unfilteredCodeLineOffsetConverter.getLine(filteredStartOffset); 151 int endLine = unfilteredCodeLineOffsetConverter.getLine(filteredEndOffset); 152 153 findingsStringBuilder.append("Offsets " + filteredStartOffset + "-" + filteredEndOffset + " (lines " + startLine 154 + "-" + endLine + "): " + message + StringUtils.LINE_SEPARATOR); 155 } 156 157 @Override 158 public void createFindingForSiblingsInCurrentFile(String message, List<OffsetBasedRegion> regions) { 159 for (OffsetBasedRegion region : regions) { 160 createFindingForOffsets(message, region.getStart(), region.getEnd(), ETextViewOption.FILTERED_CONTENT); 161 } 162 } 163 164 @Override 165 public void createFindingForSiblings(String message, CheckTextRegionLocation mainLocation, 166 List<CheckTextRegionLocation> siblingLocations) { 167 CCSMAssert.isFalse(siblingLocations.contains(mainLocation), 168 "The list of sibling locations must not contain the main location."); 169 // omitting the links to sibling findings in the test 170 createFindingForOffsets(message, mainLocation.rawStartOffset, mainLocation.rawEndOffset, 171 ETextViewOption.UNFILTERED_CONTENT); 172 } 173 174 @Override 175 protected void accessAst() { 176 assertThat(checkInfo.getParameters()) 177 .as("Check tries to access abstract syntax tree, but does not declare this as parameter.") 178 .contains(ECheckParameter.ABSTRACT_SYNTAX_TREE); 179 } 180 181 @Override 182 protected void accessTypeResolution() { 183 assertThat(checkInfo.getParameters()) 184 .as("Check tries to access type resolution, but does not declare this as parameter.") 185 .contains(ECheckParameter.TYPE_RESOLUTION); 186 } 187 188 /** Loads {@link #phaseResults} with pre-calculated test data. */ 189 /* package */ void addPhaseResult(Class<? extends IGlobalExtractionPhase<?, ?>> phaseClass, 190 Map<String, List<IExtractedValue<?>>> result) { 191 phaseResults.put(phaseClass, result); 192 } 193 194 @SuppressWarnings("unchecked") 195 @Override 196 public <T extends IExtractedValue<D>, D extends Serializable> Function<String, List<T>> accessPhaseResult( 197 Class<? extends IGlobalExtractionPhase<T, D>> phaseClass) { 198 Map<String, List<IExtractedValue<?>>> result = phaseResults.get(phaseClass); 199 CCSMAssert.isNotNull(result); 200 return uniformPath -> (List<T>) result.get(uniformPath); 201 } 202 203 /** Loads {@link #invertedPhaseResults} with pre-calculated test data. */ 204 /* package */ void addInvertedPhaseResult(Class<? extends IGlobalExtractionPhase<?, ?>> phaseClass, 205 Map<String, List<IExtractedValue<?>>> result) { 206 invertedPhaseResults.put(phaseClass, result); 207 } 208 209 @SuppressWarnings("unchecked") 210 @Override 211 public <T extends IExtractedValue<D>, D extends Serializable> Function<String, List<T>> accessPhaseInvertedResult( 212 Class<? extends IGlobalExtractionPhase<T, D>> phaseClass) { 213 try { 214 CCSMAssert.isTrue(phaseClass.newInstance().needsAccessUniformPathByValue(), 215 "Phase must return true in needsAccessUniformPathByValue to use this method."); 216 } catch (InstantiationException | IllegalAccessException e) { 217 CCSMAssert.fail("Could not instantiate check phase " + phaseClass.getSimpleName(), e); 218 } 219 Map<String, List<IExtractedValue<?>>> result = invertedPhaseResults.get(phaseClass); 220 CCSMAssert.isNotNull(result); 221 return value -> (List<T>) result.get(value); 222 } 223 224 @Override 225 public <T extends IExtractedValue<D>, D extends Serializable> List<String> listAllPhaseValues( 226 Class<? extends IGlobalExtractionPhase<T, D>> phaseClass) { 227 Map<String, List<IExtractedValue<?>>> result = invertedPhaseResults.get(phaseClass); 228 return new ArrayList<>(result.keySet()); 229 } 230}