/* * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.jstestdriver.idea.config; import com.google.common.collect.Sets; import com.google.jstestdriver.idea.util.PsiElementFragment; import com.intellij.javascript.testFramework.util.JsPsiUtils; import com.intellij.lang.annotation.Annotation; import com.intellij.lang.annotation.AnnotationHolder; import com.intellij.lang.annotation.Annotator; import com.intellij.openapi.editor.DocumentFragment; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.util.ObjectUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.yaml.YAMLTokenTypes; import org.jetbrains.yaml.YAMLUtil; import org.jetbrains.yaml.psi.*; import java.io.File; import java.util.Collection; import java.util.List; import java.util.Set; public class JstdConfigFileAnnotator implements Annotator { @Override public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { YAMLFile yamlFile = ObjectUtils.tryCast(element, YAMLFile.class); if (yamlFile != null && JstdConfigFileUtils.isJstdConfigFile(yamlFile)) { annotateFile(yamlFile, holder); } } public static void annotateFile(@NotNull YAMLFile yamlFile, @NotNull AnnotationHolder holder) { List<YAMLDocument> yamlDocuments = yamlFile.getDocuments(); boolean annotated = false; for (YAMLDocument yamlDocument : yamlDocuments) { if (annotated) { holder.createErrorAnnotation(yamlDocument, "JsTestDriver configuration file must have only one document"); } else { annotateDocument(yamlDocument, holder); } annotated = true; } } private static void annotateDocument(@NotNull YAMLDocument yamlDocument, @NotNull final AnnotationHolder holder) { final YAMLValue value = yamlDocument.getTopLevelValue(); if (!(value instanceof YAMLMapping)) { holder.createErrorAnnotation(yamlDocument, "Expected mapping"); return; } final Collection<YAMLKeyValue> keyValues = ((YAMLMapping)value).getKeyValues(); markStrangeSymbols(yamlDocument, holder); BasePathInfo basePathInfo = new BasePathInfo(yamlDocument); annotateBasePath(basePathInfo, holder); final Set<String> visitedKeys = Sets.newHashSet(); for (YAMLKeyValue keyValue : keyValues) { String keyText = keyValue.getKeyText(); if (keyValue.getKey() == null) { holder.createErrorAnnotation(keyValue.getFirstChild(), "Expected key"); continue; } if (!JstdConfigFileUtils.isTopLevelKey(keyValue)) { holder.createErrorAnnotation(keyValue.getKey(), "Unexpected key '" + keyText + "'"); } else if (!visitedKeys.add(keyText)) { holder.createErrorAnnotation(keyValue.getKey(), "Duplicated '" + keyText + "' key"); } else if (JstdConfigFileUtils.isTopLevelKeyWithInnerFileSequence(keyValue)) { annotateKeyValueWithInnerFileSequence(keyValue, holder, basePathInfo.getBasePath()); } } if (!visitedKeys.contains("test")) { Annotation annotation = holder.createWeakWarningAnnotation(yamlDocument, "JsTestDriver configuration file should have 'test:' section"); annotation.registerFix(new AddTestSectionAction()); } } private static void markStrangeSymbols(@NotNull PsiElement psiElement, @NotNull AnnotationHolder holder) { TextRange textRange = psiElement.getTextRange(); int startOffset = textRange.getStartOffset(); String text = psiElement.getText(); int specialCharactersStartOffset = -1; int specialCharactersEndOffset = -1; for (int i = 0; i < text.length(); i++) { char ch = text.charAt(i); if (ch == '\t') { int offset = startOffset + i; holder.createErrorAnnotation(new TextRange(offset, offset + 1), "Tab character is not allowed"); } else if (ch > 127) { if (specialCharactersStartOffset == -1) { specialCharactersStartOffset = startOffset + i; } specialCharactersEndOffset = startOffset + i; } if (specialCharactersEndOffset != -1 && specialCharactersEndOffset != startOffset + i) { holder.createErrorAnnotation(new TextRange(specialCharactersStartOffset, specialCharactersEndOffset + 1), "Special characters are not allowed"); specialCharactersStartOffset = -1; specialCharactersEndOffset = -1; } } if (specialCharactersStartOffset != -1) { holder.createErrorAnnotation(new TextRange(specialCharactersStartOffset, specialCharactersEndOffset + 1), "Special characters are not allowed"); } } private static void annotateBasePath(@NotNull BasePathInfo basePathInfo, @NotNull AnnotationHolder holder) { YAMLKeyValue keyValue = basePathInfo.getKeyValue(); if (keyValue != null) { DocumentFragment documentFragment = basePathInfo.getValueAsDocumentFragment(); if (documentFragment == null) { int offset = keyValue.getTextRange().getEndOffset(); holder.createErrorAnnotation(TextRange.create(offset - 1, offset), "path is unspecified"); } else { VirtualFile configDir = basePathInfo.getConfigDir(); if (configDir != null) { annotatePath(configDir, documentFragment, holder, false, true); } } } } private static void annotateKeyValueWithInnerFileSequence(@NotNull YAMLKeyValue keyValue, @NotNull final AnnotationHolder holder, @Nullable final VirtualFile basePath) { YAMLValue value = keyValue.getValue(); if (!(value instanceof YAMLSequence)) { holder.createErrorAnnotation(keyValue, "File sequence was expected here"); return; } final String indent = StringUtil.repeatSymbol(' ', YAMLUtil.getIndentInThisLine(value)); for (YAMLSequenceItem item : ((YAMLSequence)value).getItems()) { annotateFileSequence(item, holder, basePath, indent); } //final String firstIndent = toIndentString(sequence.getPrevSibling()); //sequence.acceptChildren(new PsiElementVisitor() { // @Override // public void visitElement(PsiElement element) { // final YAMLSequenceItem sequence = ObjectUtils.tryCast(element, YAMLSequenceItem.class); // if (sequence != null) { // return; // } // boolean accepted = JsPsiUtils.isElementOfType( // element, // YAMLTokenTypes.EOL, YAMLTokenTypes.WHITESPACE, YAMLTokenTypes.COMMENT, YAMLTokenTypes.INDENT // ); // accepted = accepted || element instanceof PsiWhiteSpace; // if (!accepted) { // holder.createErrorAnnotation(element, "YAML sequence was expected here"); // } // } //}); } private static void checkSequenceIndent(@NotNull YAMLSequenceItem sequence, @NotNull AnnotationHolder holder, @NotNull String expectedIndent) { PsiElement prevSibling = sequence.getPrevSibling(); if (prevSibling != null) { String indent = toIndentString(prevSibling); if (!expectedIndent.equals(indent)) { PsiElement errorElement = sequence; if (JsPsiUtils.isElementOfType(prevSibling, YAMLTokenTypes.INDENT)) { errorElement = prevSibling; } else { PsiElement firstElement = sequence.getFirstChild(); if (firstElement != null && JsPsiUtils.isElementOfType(firstElement, YAMLTokenTypes.SEQUENCE_MARKER)) { errorElement = firstElement; } } holder.createErrorAnnotation(errorElement, "All indents should be equal-sized"); } } } @NotNull private static String toIndentString(@Nullable PsiElement indentElement) { if (indentElement != null && JsPsiUtils.isElementOfType(indentElement, YAMLTokenTypes.INDENT)) { return StringUtil.notNullize(indentElement.getText()); } return ""; } private static void annotateFileSequence(@NotNull YAMLSequenceItem sequence, @NotNull final AnnotationHolder holder, @Nullable final VirtualFile basePath, @NotNull final String expectedIndent) { checkSequenceIndent(sequence, holder, expectedIndent); final YAMLValue value = sequence.getValue(); if (value == null) { holder.createErrorAnnotation(sequence, "Sequence item is empty"); return; } if (value instanceof YAMLSequence) { for (YAMLSequenceItem item : ((YAMLSequence)value).getItems()) { annotateFileSequence(item, holder, basePath, expectedIndent); } } if (value instanceof YAMLScalar && ((YAMLScalar)value).isMultiline()) { holder.createErrorAnnotation(sequence, "Unexpected multiline path"); return; } PsiElementFragment<YAMLSequenceItem> sequenceTextFragment = JstdConfigFileUtils.buildSequenceTextFragment(sequence); if (basePath != null && sequenceTextFragment != null) { DocumentFragment documentFragment = sequenceTextFragment.toDocumentFragment(); if (documentFragment != null) { annotatePath(basePath, documentFragment, holder, true, false); } } } private static void annotatePath(@NotNull VirtualFile basePath, @NotNull DocumentFragment pathAsDocumentFragment, @NotNull final AnnotationHolder holder, boolean tolerateRemoteLocations, boolean expectDirectory) { String pathStr = pathAsDocumentFragment.getDocument().getText(pathAsDocumentFragment.getTextRange()); if (tolerateRemoteLocations && (pathStr.startsWith("http:") || pathStr.startsWith("https:"))) { return; } if (StringUtil.isEmptyOrSpaces(pathStr)) { holder.createErrorAnnotation(pathAsDocumentFragment.getTextRange(), "Malformed path"); return; } int documentOffset = pathAsDocumentFragment.getTextRange().getStartOffset(); List<String> components = JstdConfigFileUtils.convertPathToComponentList(pathStr); VirtualFile current = basePath; if (!components.isEmpty()) { String first = components.get(0); if (first.length() + 1 <= pathStr.length()) { first = pathStr.substring(0, first.length() + 1); } if (!first.isEmpty()) { VirtualFile initial = BasePathInfo.findFile(basePath, first); if (initial != null) { current = initial; components = components.subList(1, components.size()); documentOffset += first.length(); } } } File currentFile = new File(current.getPath()); for (String component : components) { if (component.contains("*")) { return; } if (component.isEmpty()) { holder.createErrorAnnotation(TextRange.create(documentOffset, documentOffset + 1), "Malformed path"); return; } File next = new File(currentFile, component); if (!next.exists()) { holder.createErrorAnnotation(TextRange.create(documentOffset, documentOffset + component.length()), "No such file or directory '" + component + "'"); return; } documentOffset += component.length() + 1; currentFile = next; } if (expectDirectory) { if (!current.isDirectory()) { holder.createErrorAnnotation(pathAsDocumentFragment.getTextRange(), "A directory is expected"); } } else { if (currentFile.isDirectory()) { holder.createErrorAnnotation(pathAsDocumentFragment.getTextRange(), "A file is expected"); } } } }