/*
* 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.google.jstestdriver.idea.util.CastUtils;
import com.google.jstestdriver.idea.util.JsPsiUtils;
import com.google.jstestdriver.idea.util.PsiElementFragment;
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.PsiElement;
import com.intellij.psi.PsiElementVisitor;
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 = CastUtils.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> documents = yamlFile.getDocuments();
boolean annotated = false;
for (YAMLDocument document : documents) {
if (annotated) {
holder.createErrorAnnotation(document, "JsTestDriver Configuration File must have only one document");
} else {
annotateDocument(document, holder);
}
annotated = true;
}
}
private static void annotateDocument(@NotNull YAMLDocument yamlDocument, @NotNull final AnnotationHolder holder) {
List<Group> groups = buildGroups(yamlDocument);
if (groups == null) {
return;
}
BasePathInfo basePathInfo = new BasePathInfo(yamlDocument);
annotateBasePath(basePathInfo, holder);
final Set<String> visitedKeys = Sets.newHashSet();
for (Group group : groups) {
YAMLKeyValue keyValue = group.getKeyValue();
if (keyValue != null) {
PsiElement keyElement = keyValue.getKey();
String keyStr = keyValue.getKeyText();
if (!JstdConfigFileUtils.isTopLevelKey(keyValue)) {
holder.createErrorAnnotation(keyElement, "Unexpected key '" + keyStr + "'");
}
if (!visitedKeys.add(keyStr)) {
holder.createErrorAnnotation(keyElement, "Duplicated '" + keyStr + "' key");
} else if (JstdConfigFileUtils.isTopLevelKeyWithInnerFileSequence(keyValue)) {
annotateKeyValueWithInnerFileSequence(keyValue, holder, basePathInfo.getBasePath());
}
} else {
PsiElement element = group.getUnexpectedElement();
if (!JsPsiUtils.isElementOfType(element, YAMLTokenTypes.EOL, YAMLTokenTypes.INDENT)) {
holder.createErrorAnnotation(element, "Unexpected element '" + element.getText() + "'");
}
}
}
}
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) {
YAMLCompoundValue compoundValue = CastUtils.tryCast(keyValue.getValue(), YAMLCompoundValue.class);
if (compoundValue == null) {
holder.createErrorAnnotation(keyValue, "YAML sequence was expected here");
return;
}
PsiElement firstIndentElement = compoundValue.getPrevSibling();
if (firstIndentElement == null || !JsPsiUtils.isElementOfType(firstIndentElement, YAMLTokenTypes.INDENT)) {
int offset = compoundValue.getTextRange().getStartOffset();
holder.createErrorAnnotation(TextRange.create(offset, offset), "Indent was expected here");
return;
}
final String firstIndent = StringUtil.notNullize(firstIndentElement.getText());
compoundValue.acceptChildren(new PsiElementVisitor() {
@Override
public void visitElement(PsiElement element) {
final YAMLSequence sequence = CastUtils.tryCast(element, YAMLSequence.class);
if (sequence != null) {
annotateFileSequence(sequence, holder, basePath);
return;
}
boolean indentType = JsPsiUtils.isElementOfType(element, YAMLTokenTypes.INDENT);
boolean whitespaceType = JsPsiUtils.isElementOfType(element, YAMLTokenTypes.EOL, YAMLTokenTypes.WHITESPACE);
if (indentType || whitespaceType) {
if (indentType && !firstIndent.equals(element.getText())) {
holder.createErrorAnnotation(element, "All indents should be equal-sized");
}
} else {
holder.createErrorAnnotation(element, "YAML sequence was expected here");
}
}
});
}
private static void annotateFileSequence(@NotNull YAMLSequence sequence,
@NotNull AnnotationHolder holder,
@Nullable VirtualFile basePath) {
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<Group> buildGroups(@NotNull YAMLDocument yamlDocument) {
final Document document = JsPsiUtils.getDocument(yamlDocument);
if (document == null) {
return null;
}
final List<Group> groups = Lists.newArrayList();
final Ref<Integer> previousKeyValueEndLineNumberRef = Ref.create(-1);
yamlDocument.acceptChildren(new PsiElementVisitor() {
@Override
public void visitElement(PsiElement element) {
int startLineNumber = JstdConfigFileUtils.getStartLineNumber(document, element);
if (previousKeyValueEndLineNumberRef.get() < startLineNumber) {
if (element instanceof YAMLKeyValue) {
YAMLKeyValue yamlKeyValue = (YAMLKeyValue)element;
previousKeyValueEndLineNumberRef.set(JstdConfigFileUtils.getEndLineNumber(document, yamlKeyValue));
groups.add(new Group(yamlKeyValue, null));
}
else {
groups.add(new Group(null, element));
}
}
}
});
return groups;
}
private static class Group {
private final YAMLKeyValue myKeyValue;
private final PsiElement myUnexpectedElement;
private Group(YAMLKeyValue keyValue, PsiElement unexpectedElement) {
myKeyValue = keyValue;
myUnexpectedElement = unexpectedElement;
}
public YAMLKeyValue getKeyValue() {
return myKeyValue;
}
public PsiElement getUnexpectedElement() {
return myUnexpectedElement;
}
}
}