375ad5b973e1e4dcbb5877f817b4731c44b12148
[idea/community.git] / platform / testFramework / src / com / intellij / testFramework / ParsingTestCase.java
1 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.testFramework;
3
4 import com.intellij.ide.startup.impl.StartupManagerImpl;
5 import com.intellij.lang.*;
6 import com.intellij.lang.impl.PsiBuilderFactoryImpl;
7 import com.intellij.lang.injection.InjectedLanguageManager;
8 import com.intellij.lang.injection.MultiHostInjector;
9 import com.intellij.mock.*;
10 import com.intellij.openapi.application.ex.PathManagerEx;
11 import com.intellij.openapi.editor.EditorFactory;
12 import com.intellij.openapi.extensions.DefaultPluginDescriptor;
13 import com.intellij.openapi.extensions.ExtensionPoint;
14 import com.intellij.openapi.extensions.ExtensionPointName;
15 import com.intellij.openapi.extensions.PluginDescriptor;
16 import com.intellij.openapi.extensions.impl.ExtensionPointImpl;
17 import com.intellij.openapi.extensions.impl.ExtensionsAreaImpl;
18 import com.intellij.openapi.fileEditor.FileDocumentManager;
19 import com.intellij.openapi.fileEditor.impl.FileDocumentManagerImpl;
20 import com.intellij.openapi.fileEditor.impl.LoadTextUtil;
21 import com.intellij.openapi.fileTypes.FileTypeFactory;
22 import com.intellij.openapi.fileTypes.FileTypeManager;
23 import com.intellij.openapi.options.SchemeManagerFactory;
24 import com.intellij.openapi.progress.EmptyProgressIndicator;
25 import com.intellij.openapi.progress.ProgressManager;
26 import com.intellij.openapi.progress.impl.ProgressManagerImpl;
27 import com.intellij.openapi.startup.StartupManager;
28 import com.intellij.openapi.util.TextRange;
29 import com.intellij.openapi.util.io.FileUtil;
30 import com.intellij.openapi.util.text.LineColumn;
31 import com.intellij.openapi.util.text.StringUtil;
32 import com.intellij.openapi.vfs.CharsetToolkit;
33 import com.intellij.pom.PomModel;
34 import com.intellij.pom.core.impl.PomModelImpl;
35 import com.intellij.pom.tree.TreeAspect;
36 import com.intellij.psi.*;
37 import com.intellij.psi.impl.*;
38 import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry;
39 import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistryImpl;
40 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageManagerImpl;
41 import com.intellij.psi.util.CachedValuesManager;
42 import com.intellij.util.CachedValuesManagerImpl;
43 import com.intellij.util.KeyedLazyInstance;
44 import com.intellij.util.containers.ContainerUtil;
45 import com.intellij.util.messages.MessageBus;
46 import org.jetbrains.annotations.NotNull;
47 import org.picocontainer.ComponentAdapter;
48 import org.picocontainer.MutablePicoContainer;
49
50 import java.io.File;
51 import java.io.IOException;
52 import java.nio.charset.StandardCharsets;
53 import java.util.*;
54
55 /** @noinspection JUnitTestCaseWithNonTrivialConstructors*/
56 public abstract class ParsingTestCase extends UsefulTestCase {
57   private PluginDescriptor myPluginDescriptor;
58
59   private MockApplication myApp;
60   protected MockProjectEx myProject;
61
62   protected String myFilePrefix = "";
63   protected String myFileExt;
64   protected final String myFullDataPath;
65   protected PsiFile myFile;
66   private MockPsiManager myPsiManager;
67   private PsiFileFactoryImpl myFileFactory;
68   protected Language myLanguage;
69   private final ParserDefinition[] myDefinitions;
70   private final boolean myLowercaseFirstLetter;
71   private ExtensionPointImpl<KeyedLazyInstance<ParserDefinition>> myLangParserDefinition;
72
73   protected ParsingTestCase(@NotNull String dataPath, @NotNull String fileExt, @NotNull ParserDefinition... definitions) {
74     this(dataPath, fileExt, false, definitions);
75   }
76
77   protected ParsingTestCase(@NotNull String dataPath, @NotNull String fileExt, boolean lowercaseFirstLetter, @NotNull ParserDefinition... definitions) {
78     myDefinitions = definitions;
79     myFullDataPath = getTestDataPath() + "/" + dataPath;
80     myFileExt = fileExt;
81     myLowercaseFirstLetter = lowercaseFirstLetter;
82   }
83
84   @NotNull
85   protected MockApplication getApplication() {
86     return myApp;
87   }
88
89   @Override
90   protected void setUp() throws Exception {
91     super.setUp();
92
93     MockApplication app = MockApplication.setUp(getTestRootDisposable());
94     myApp = app;
95     MutablePicoContainer appContainer = app.getPicoContainer();
96     ComponentAdapter component = appContainer.getComponentAdapter(ProgressManager.class.getName());
97     if (component == null) {
98       appContainer.registerComponentInstance(ProgressManager.class.getName(), new ProgressManagerImpl());
99     }
100
101     myProject = new MockProjectEx(getTestRootDisposable());
102     myPsiManager = new MockPsiManager(myProject);
103     myFileFactory = new PsiFileFactoryImpl(myPsiManager);
104     appContainer.registerComponentInstance(MessageBus.class, app.getMessageBus());
105     appContainer.registerComponentInstance(SchemeManagerFactory.class, new MockSchemeManagerFactory());
106     MockEditorFactory editorFactory = new MockEditorFactory();
107     appContainer.registerComponentInstance(EditorFactory.class, editorFactory);
108     app.registerService(FileDocumentManager.class, new MockFileDocumentManagerImpl(charSequence -> {
109       return editorFactory.createDocument(charSequence);
110     }, FileDocumentManagerImpl.HARD_REF_TO_DOCUMENT_KEY));
111     appContainer.registerComponentInstance(PsiDocumentManager.class, new MockPsiDocumentManager());
112
113     app.registerService(PsiBuilderFactory.class, new PsiBuilderFactoryImpl());
114     app.registerService(DefaultASTFactory.class, new DefaultASTFactoryImpl());
115     app.registerService(ReferenceProvidersRegistry.class, new ReferenceProvidersRegistryImpl());
116     myProject.registerService(CachedValuesManager.class, new CachedValuesManagerImpl(myProject, new PsiCachedValuesFactory(myPsiManager)));
117     myProject.registerService(PsiManager.class, myPsiManager);
118     myProject.registerService(StartupManager.class, new StartupManagerImpl(myProject));
119     registerExtensionPoint(app.getExtensionArea(), FileTypeFactory.FILE_TYPE_FACTORY_EP, FileTypeFactory.class);
120     registerExtensionPoint(app.getExtensionArea(), MetaLanguage.EP_NAME, MetaLanguage.class);
121
122     myLangParserDefinition = app.getExtensionArea().registerFakeBeanPoint(LanguageParserDefinitions.INSTANCE.getName(), getPluginDescriptor());
123
124     if (myDefinitions.length > 0) {
125       configureFromParserDefinition(myDefinitions[0], myFileExt);
126       // first definition is registered by configureFromParserDefinition
127       for (int i = 1, length = myDefinitions.length; i < length; i++) {
128         registerParserDefinition(myDefinitions[i]);
129       }
130     }
131
132     // That's for reparse routines
133     PomModelImpl pomModel = new PomModelImpl(myProject);
134     myProject.registerService(PomModel.class, pomModel);
135     new TreeAspect(pomModel);
136   }
137
138   protected final void registerParserDefinition(@NotNull ParserDefinition definition) {
139     final Language language = definition.getFileNodeType().getLanguage();
140     myLangParserDefinition.registerExtension(new KeyedLazyInstance<ParserDefinition>() {
141       @Override
142       public String getKey() {
143         return language.getID();
144       }
145
146       @NotNull
147       @Override
148       public ParserDefinition getInstance() {
149         return definition;
150       }
151     });
152     LanguageParserDefinitions.INSTANCE.clearCache(language);
153     disposeOnTearDown(() -> LanguageParserDefinitions.INSTANCE.clearCache(language));
154   }
155
156   public void configureFromParserDefinition(@NotNull ParserDefinition definition, String extension) {
157     myLanguage = definition.getFileNodeType().getLanguage();
158     myFileExt = extension;
159     registerParserDefinition(definition);
160     myApp.getPicoContainer().registerComponentInstance(FileTypeManager.class, new MockFileTypeManager(new MockLanguageFileType(myLanguage, myFileExt)));
161   }
162
163   protected final <T> void registerExtension(@NotNull ExtensionPointName<T> name, @NotNull T extension) {
164     //noinspection unchecked
165     registerExtensions(name, (Class<T>)extension.getClass(), Collections.singletonList(extension));
166   }
167
168   protected final <T> void registerExtensions(@NotNull ExtensionPointName<T> name, @NotNull Class<T> extensionClass, @NotNull List<T> extensions) {
169     ExtensionsAreaImpl area = myApp.getExtensionArea();
170     ExtensionPoint<T> point = area.getExtensionPointIfRegistered(name.getName());
171     if (point == null) {
172       point = registerExtensionPoint(area, name, extensionClass);
173     }
174
175     for (T extension : extensions) {
176       // no need to specify disposable because ParsingTestCase in any case clean area for each test
177       //noinspection deprecation
178       point.registerExtension(extension);
179     }
180   }
181
182   protected final <T> void addExplicitExtension(@NotNull LanguageExtension<T> collector, @NotNull Language language, @NotNull T object) {
183     ExtensionsAreaImpl area = myApp.getExtensionArea();
184     if (!area.hasExtensionPoint(collector.getName())) {
185       area.registerFakeBeanPoint(collector.getName(), getPluginDescriptor());
186     }
187     ExtensionTestUtil.addExtension(area, collector, language, object);
188   }
189
190   protected final <T> void registerExtensionPoint(@NotNull ExtensionPointName<T> extensionPointName, @NotNull Class<T> aClass) {
191     registerExtensionPoint(myApp.getExtensionArea(), extensionPointName, aClass);
192   }
193
194   protected <T> ExtensionPointImpl<T> registerExtensionPoint(@NotNull ExtensionsAreaImpl extensionArea,
195                                                              @NotNull ExtensionPointName<T> extensionPointName,
196                                                              @NotNull Class<T> extensionClass) {
197     // todo get rid of it - registerExtensionPoint should be not called several times
198     String name = extensionPointName.getName();
199     if (extensionArea.hasExtensionPoint(name)) {
200       return extensionArea.getExtensionPoint(name);
201     }
202     else {
203       return extensionArea.registerPoint(name, extensionClass, getPluginDescriptor());
204     }
205   }
206
207   @NotNull
208   // easy debug of not disposed extension
209   private PluginDescriptor getPluginDescriptor() {
210     PluginDescriptor pluginDescriptor = myPluginDescriptor;
211     if (pluginDescriptor == null) {
212       pluginDescriptor = new DefaultPluginDescriptor(getClass().getName() + "." + getName());
213       myPluginDescriptor = pluginDescriptor;
214     }
215     return pluginDescriptor;
216   }
217
218   @NotNull
219   public MockProjectEx getProject() {
220     return myProject;
221   }
222
223   public MockPsiManager getPsiManager() {
224     return myPsiManager;
225   }
226
227   @Override
228   protected void tearDown() throws Exception {
229     myFile = null;
230     myProject = null;
231     myPsiManager = null;
232     myFileFactory = null;
233     super.tearDown();
234   }
235
236   protected String getTestDataPath() {
237     return PathManagerEx.getTestDataPath();
238   }
239
240   @NotNull
241   public final String getTestName() {
242     return getTestName(myLowercaseFirstLetter);
243   }
244
245   protected boolean includeRanges() {
246     return false;
247   }
248
249   protected boolean skipSpaces() {
250     return false;
251   }
252
253   protected boolean checkAllPsiRoots() {
254     return true;
255   }
256
257   /* Sanity check against thoughtlessly copy-pasting actual test results as the expected test data. */
258   protected void ensureNoErrorElements() {
259     myFile.accept(new PsiRecursiveElementVisitor() {
260       private static final int TAB_WIDTH = 8;
261
262       @Override
263       public void visitErrorElement(PsiErrorElement element) {
264         // Very dump approach since a corresponding Document is not available.
265         String text = myFile.getText();
266         String[] lines = StringUtil.splitByLinesKeepSeparators(text);
267
268         int offset = element.getTextOffset();
269         LineColumn position = StringUtil.offsetToLineColumn(text, offset);
270         int lineNumber = position != null ? position.line : -1;
271         int column = position != null ? position.column : 0;
272
273         String line = StringUtil.trimTrailing(lines[lineNumber]);
274         // Sanitize: expand indentation tabs, replace the rest with a single space
275         int numIndentTabs = StringUtil.countChars(line.subSequence(0, column), '\t', 0, true);
276         int indentedColumn = column + numIndentTabs * (TAB_WIDTH - 1);
277         String lineWithNoTabs = StringUtil.repeat(" ", numIndentTabs * TAB_WIDTH) + line.substring(numIndentTabs).replace('\t', ' ');
278         String errorUnderline = StringUtil.repeat(" ", indentedColumn) + StringUtil.repeat("^", Math.max(1, element.getTextLength()));
279
280         fail(String.format("Unexpected error element: %s:%d:%d\n\n%s\n%s\n%s",
281                            myFile.getName(), lineNumber + 1, column,
282                            lineWithNoTabs, errorUnderline, element.getErrorDescription()));
283       }
284     });
285   }
286
287   protected void doTest(boolean checkResult) {
288     doTest(checkResult, false);
289   }
290
291   protected void doTest(boolean checkResult, boolean ensureNoErrorElements) {
292     String name = getTestName();
293     try {
294       String text = loadFile(name + "." + myFileExt);
295       myFile = createPsiFile(name, text);
296       ensureParsed(myFile);
297       assertEquals("light virtual file text mismatch", text, ((LightVirtualFile)myFile.getVirtualFile()).getContent().toString());
298       assertEquals("virtual file text mismatch", text, LoadTextUtil.loadText(myFile.getVirtualFile()));
299       assertEquals("doc text mismatch", text, Objects.requireNonNull(myFile.getViewProvider().getDocument()).getText());
300       assertEquals("psi text mismatch", text, myFile.getText());
301       ensureCorrectReparse(myFile);
302       if (checkResult) {
303         checkResult(name, myFile);
304         if (ensureNoErrorElements) {
305           ensureNoErrorElements();
306         }
307       }
308       else {
309         toParseTreeText(myFile, skipSpaces(), includeRanges());
310       }
311     }
312     catch (IOException e) {
313       throw new RuntimeException(e);
314     }
315   }
316
317   protected void doTest(String suffix) throws IOException {
318     String name = getTestName();
319     String text = loadFile(name + "." + myFileExt);
320     myFile = createPsiFile(name, text);
321     ensureParsed(myFile);
322     assertEquals(text, myFile.getText());
323     checkResult(name + suffix, myFile);
324   }
325
326   protected void doCodeTest(@NotNull String code) throws IOException {
327     String name = getTestName();
328     myFile = createPsiFile("a", code);
329     ensureParsed(myFile);
330     assertEquals(code, myFile.getText());
331     checkResult(myFilePrefix + name, myFile);
332   }
333
334   protected PsiFile createPsiFile(@NotNull String name, @NotNull String text) {
335     return createFile(name + "." + myFileExt, text);
336   }
337
338   protected PsiFile createFile(@NotNull String name, @NotNull String text) {
339     LightVirtualFile virtualFile = new LightVirtualFile(name, myLanguage, text);
340     virtualFile.setCharset(StandardCharsets.UTF_8);
341     return createFile(virtualFile);
342   }
343
344   protected PsiFile createFile(@NotNull LightVirtualFile virtualFile) {
345     return myFileFactory.trySetupPsiForFile(virtualFile, myLanguage, true, false);
346   }
347
348   protected void checkResult(@NotNull @TestDataFile String targetDataName, @NotNull PsiFile file) throws IOException {
349     doCheckResult(myFullDataPath, file, checkAllPsiRoots(), targetDataName, skipSpaces(), includeRanges(), allTreesInSingleFile());
350   }
351
352   protected boolean allTreesInSingleFile() {
353     return false;
354   }
355
356   public static void doCheckResult(@NotNull String testDataDir,
357                                    @NotNull PsiFile file,
358                                    boolean checkAllPsiRoots,
359                                    @NotNull String targetDataName,
360                                    boolean skipSpaces,
361                                    boolean printRanges) {
362     doCheckResult(testDataDir, file, checkAllPsiRoots, targetDataName, skipSpaces, printRanges, false);
363   }
364
365   public static void doCheckResult(@NotNull String testDataDir,
366                                    @NotNull PsiFile file,
367                                    boolean checkAllPsiRoots,
368                                    @NotNull String targetDataName,
369                                    boolean skipSpaces,
370                                    boolean printRanges,
371                                    boolean allTreesInSingleFile) {
372     FileViewProvider provider = file.getViewProvider();
373     Set<Language> languages = provider.getLanguages();
374
375     if (!checkAllPsiRoots || languages.size() == 1) {
376       doCheckResult(testDataDir, targetDataName + ".txt", toParseTreeText(file, skipSpaces, printRanges).trim());
377       return;
378     }
379
380     if (allTreesInSingleFile) {
381       String expectedName = targetDataName + ".txt";
382       StringBuilder sb = new StringBuilder();
383       List<Language> languagesList = new ArrayList<>(languages);
384       ContainerUtil.sort(languagesList, Comparator.comparing(Language::getID));
385       for (Language language : languagesList) {
386         sb.append("Subtree: ").append(language.getDisplayName()).append(" (").append(language.getID()).append(")").append("\n")
387           .append(toParseTreeText(provider.getPsi(language), skipSpaces, printRanges).trim())
388           .append("\n").append(StringUtil.repeat("-", 80)).append("\n");
389       }
390       doCheckResult(testDataDir, expectedName, sb.toString());
391     }
392     else {
393       for (Language language : languages) {
394         PsiFile root = provider.getPsi(language);
395         assertNotNull("FileViewProvider " + provider + " didn't return PSI root for language " + language.getID(), root);
396         String expectedName = targetDataName + "." + language.getID() + ".txt";
397         doCheckResult(testDataDir, expectedName, toParseTreeText(root, skipSpaces, printRanges).trim());
398       }
399     }
400   }
401
402   protected void checkResult(@NotNull String actual) {
403     String name = getTestName();
404     doCheckResult(myFullDataPath, myFilePrefix + name + ".txt", actual);
405   }
406
407   protected void checkResult(@NotNull @TestDataFile String targetDataName, @NotNull String actual) {
408     doCheckResult(myFullDataPath, targetDataName, actual);
409   }
410
411   public static void doCheckResult(@NotNull String fullPath, @NotNull String targetDataName, @NotNull String actual) {
412     String expectedFileName = fullPath + File.separatorChar + targetDataName;
413     UsefulTestCase.assertSameLinesWithFile(expectedFileName, actual);
414   }
415
416   protected static String toParseTreeText(@NotNull PsiElement file,  boolean skipSpaces, boolean printRanges) {
417     return DebugUtil.psiToString(file, skipSpaces, printRanges);
418   }
419
420   protected String loadFile(@NotNull @TestDataFile String name) throws IOException {
421     return loadFileDefault(myFullDataPath, name);
422   }
423
424   public static String loadFileDefault(@NotNull String dir, @NotNull String name) throws IOException {
425     return FileUtil.loadFile(new File(dir, name), CharsetToolkit.UTF8, true).trim();
426   }
427
428   public static void ensureParsed(@NotNull PsiFile file) {
429     file.accept(new PsiElementVisitor() {
430       @Override
431       public void visitElement(PsiElement element) {
432         element.acceptChildren(this);
433       }
434     });
435   }
436
437   public static void ensureCorrectReparse(@NotNull final PsiFile file) {
438     final String psiToStringDefault = DebugUtil.psiToString(file, false, false);
439
440     DebugUtil.performPsiModification("ensureCorrectReparse", () -> {
441                                        final String fileText = file.getText();
442                                        final DiffLog diffLog = new BlockSupportImpl().reparseRange(
443                                          file, file.getNode(), TextRange.allOf(fileText), fileText, new EmptyProgressIndicator(), fileText);
444                                        diffLog.performActualPsiChange(file);
445                                      });
446
447     assertEquals(psiToStringDefault, DebugUtil.psiToString(file, false, false));
448   }
449
450   public void registerMockInjectedLanguageManager() {
451     registerExtensionPoint(myProject.getExtensionArea(), MultiHostInjector.MULTIHOST_INJECTOR_EP_NAME, MultiHostInjector.class);
452
453     registerExtensionPoint(myApp.getExtensionArea(), LanguageInjector.EXTENSION_POINT_NAME, LanguageInjector.class);
454     myProject.registerService(InjectedLanguageManager.class, new InjectedLanguageManagerImpl(myProject, new MockDumbService(myProject)));
455   }
456 }