Package com.google.jstestdriver.idea.config

Source Code of com.google.jstestdriver.idea.config.JstdConfigFileAnnotator

/*
* 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.Lists;
import com.google.common.collect.Sets;
import com.intellij.javascript.testFramework.util.JsPsiUtils;
import com.google.jstestdriver.idea.util.PsiElementFragment;
import com.intellij.lang.annotation.Annotation;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.DocumentFragment;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.util.ObjectUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.yaml.YAMLTokenTypes;
import org.jetbrains.yaml.psi.*;

import java.io.File;
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) {
    List<YAMLKeyValue> keyValues = fetchKeyValues(yamlDocument, holder);
    if (keyValues == null) {
      return;
    }
    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 (!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) {
    PsiElement value = keyValue.getValue();
    if (value == null) {
      holder.createErrorAnnotation(keyValue, "File sequence was expected here");
      return;
    }
    YAMLCompoundValue compoundValue = ObjectUtils.tryCast(value, YAMLCompoundValue.class);
    if (compoundValue == null) {
      holder.createErrorAnnotation(value, "File sequence should start with a dash symbol");
      return;
    }
    final String firstIndent = toIndentString(compoundValue.getPrevSibling());
    compoundValue.acceptChildren(new PsiElementVisitor() {
      @Override
      public void visitElement(PsiElement element) {
        final YAMLSequence sequence = ObjectUtils.tryCast(element, YAMLSequence.class);
        if (sequence != null) {
          annotateFileSequence(sequence, holder, basePath, firstIndent);
          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 YAMLSequence 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 YAMLSequence sequence,
                                           @NotNull final AnnotationHolder holder,
                                           @Nullable final VirtualFile basePath,
                                           @NotNull final String expectedIndent) {
    checkSequenceIndent(sequence, holder, expectedIndent);
    sequence.acceptChildren(new PsiElementVisitor() {
      @Override
      public void visitElement(PsiElement element) {
        if (element instanceof YAMLSequence) {
          YAMLSequence childSequence = (YAMLSequence) element;
          annotateFileSequence(childSequence, holder, basePath, expectedIndent);
        }
        super.visitElement(element);
      }
    });
    if (!isOneLineText(sequence)) {
      holder.createErrorAnnotation(sequence, "Unexpected multiline path");
      return;
    }
    PsiElementFragment<YAMLSequence> sequenceTextFragment = JstdConfigFileUtils.buildSequenceTextFragment(sequence);
    if (basePath != null && sequenceTextFragment != null) {
      DocumentFragment documentFragment = sequenceTextFragment.toDocumentFragment();
      if (documentFragment != null) {
        annotatePath(basePath, documentFragment, holder, true, false);
      }
    }
  }

  private static boolean isOneLineText(@NotNull YAMLSequence sequence) {
    PsiElementFragment<YAMLSequence> textSequenceFragment = JstdConfigFileUtils.buildSequenceTextFragment(sequence);
    if (textSequenceFragment != null) {
      DocumentFragment textFragment = textSequenceFragment.toDocumentFragment();
      if (textFragment != null) {
        Document document = textFragment.getDocument();
        TextRange textRange = textFragment.getTextRange();
        int startLine = document.getLineNumber(textRange.getStartOffset());
        int endLine = document.getLineNumber(textRange.getEndOffset());
        return startLine == endLine;
      }
    }
    return 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");
      }
    }
  }

  @Nullable
  private static List<YAMLKeyValue> fetchKeyValues(@NotNull YAMLDocument yamlDocument,
                                                   @NotNull final AnnotationHolder holder) {
    final Document document = JsPsiUtils.getDocument(yamlDocument);
    if (document == null) {
      return null;
    }
    final List<YAMLKeyValue> keyValues = Lists.newArrayList();
    final Ref<Integer> previousKeyValueEndLineNumberRef = Ref.create(-1);
    yamlDocument.acceptChildren(new PsiElementVisitor() {
      @Override
      public void visitElement(PsiElement element) {
        if (JsPsiUtils.isElementOfType(element, YAMLTokenTypes.EOL, YAMLTokenTypes.INDENT)
            || element instanceof PsiComment) {
          return;
        }
        int startLineNumber = JstdConfigFileUtils.getStartLineNumber(document, element);
        if (previousKeyValueEndLineNumberRef.get() < startLineNumber) {
          if (element instanceof YAMLKeyValue) {
            YAMLKeyValue yamlKeyValue = (YAMLKeyValue)element;
            int endOffset = yamlKeyValue.getTextRange().getEndOffset();
            int endLine = document.getLineNumber(Math.max(0, endOffset - 1));
            previousKeyValueEndLineNumberRef.set(endLine);
            keyValues.add(yamlKeyValue);
          }
          else {
            holder.createErrorAnnotation(element, "Unexpected element '" + element.getText() + "'");
          }
        }
      }
    });
    return keyValues;
  }
}
TOP

Related Classes of com.google.jstestdriver.idea.config.JstdConfigFileAnnotator

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.