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}