/**
* Copyright (c) 2013 Puppet Labs, Inc. and other contributors, as listed below.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Puppet Labs
*/
package com.puppetlabs.geppetto.pp.dsl.ppdoc;
import java.util.List;
import com.puppetlabs.geppetto.pp.Definition;
import com.puppetlabs.geppetto.pp.HostClassDefinition;
import com.puppetlabs.geppetto.pp.NodeDefinition;
import com.puppetlabs.geppetto.pp.dsl.PPDSLConstants;
import com.puppetlabs.geppetto.pp.dsl.adapters.ResourceDocumentationAdapter;
import com.puppetlabs.geppetto.pp.dsl.adapters.ResourceDocumentationAdapterFactory;
import com.puppetlabs.geppetto.pp.dsl.adapters.ResourcePropertiesAdapter;
import com.puppetlabs.geppetto.pp.dsl.adapters.ResourcePropertiesAdapterFactory;
import com.puppetlabs.geppetto.pp.dsl.linking.IMessageAcceptor;
import com.puppetlabs.geppetto.pp.dsl.linking.PPTask;
import com.puppetlabs.geppetto.pp.dsl.services.PPGrammarAccess;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.xtext.IGrammarAccess;
import org.eclipse.xtext.TerminalRule;
import org.eclipse.xtext.nodemodel.BidiTreeIterator;
import org.eclipse.xtext.nodemodel.ICompositeNode;
import org.eclipse.xtext.nodemodel.INode;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
/**
* Provides handling of documentation comments.
*
*/
public class DocumentationAssociator {
private final PPGrammarAccess ga;
/**
* Expression that may have associated documentation. (TODO: Puppetdoc also lists Nodes global variables, custom facts, and
* Puppet plugins located in modules - but don't know which of those are applicable).
*/
private static final Class<?>[] documentable = { HostClassDefinition.class, Definition.class, NodeDefinition.class, };
private static final String[] defaultTaskTags = new String[] { "todo", "fixme" };
@Inject
public DocumentationAssociator(IGrammarAccess ga) {
this.ga = (PPGrammarAccess) ga;
}
private void associateDocumentation(EObject semantic, List<INode> commentSequence) {
StringBuffer buf = new StringBuffer();
for(INode n : commentSequence)
buf.append(n.getText());
ResourceDocumentationAdapter adapter = ResourceDocumentationAdapterFactory.eINSTANCE.adapt(semantic.eResource());
adapter.put(semantic, commentSequence);
}
private void associateTasks(EObject model, List<PPTask> tasks) {
Resource r = model.eResource();
if(r == null)
return; // not in a resource, sorry.
ResourcePropertiesAdapter adapter = ResourcePropertiesAdapterFactory.eINSTANCE.adapt(r);
adapter.put(PPDSLConstants.RESOURCE_PROPERTY__TASK_LIST, tasks);
}
private void clearDocumentation(EObject model) {
Resource r = model.eResource();
if(r == null)
return;
ResourceDocumentationAdapter adapter = ResourceDocumentationAdapterFactory.eINSTANCE.adapt(r);
if(adapter != null)
adapter.clear();
}
public List<INode> getDocumentation(EObject semantic) {
Resource r = semantic.eResource();
if(r == null)
return null;
ResourceDocumentationAdapter adapter = ResourceDocumentationAdapterFactory.eINSTANCE.adapt(r);
return adapter != null
? adapter.get(semantic)
: null;
}
private String[] getTaskTags() {
return defaultTaskTags;
}
/**
* @param text
* @param node
* @return
*/
private boolean hasNonWSBeforeStart(String text, INode node) {
int offset = node.getOffset();
for(int pos = offset - 1; pos >= 0; pos--) {
char c = text.charAt(pos);
if(c == '\r' || c == '\n')
return false;
if(c > ' ')
return true;
}
// reached start of parsed text
return false;
}
/**
* Links comment nodes to classes listed in {@link #documentable} by collecting them in an
* adapter (for later processing by formatter/styler).
*
*/
public void linkDocumentation(EObject model) {
// clear stored tasks
associateTasks(model, null);
clearDocumentation(model);
List<PPTask> tasks = Lists.newArrayList();
final TerminalRule mlRule = ga.getML_COMMENTRule();
final TerminalRule slRule = ga.getSL_COMMENTRule();
final TerminalRule wsRule = ga.getWSRule();
// a sequence of SL comment or a single ML comment that is immediately (no NL) before
// a definition, class, or node is taken to be a documentation comment, as is associated with
// the following semantic object using an adapter.
//
ICompositeNode node = NodeModelUtils.getNode(model);
ICompositeNode root = node.getRootNode();
List<INode> commentSequence = Lists.newArrayList();
BidiTreeIterator<INode> itor = root.getAsTreeIterable().iterator();
COLLECT_LOOP: while(itor.hasNext()) {
// for(INode x : root.getAsTreeIterable()) {
INode x = itor.next();
EObject grammarElement = x.getGrammarElement();
// process comments
if(grammarElement == slRule || grammarElement == mlRule) {
processCommentNode(x, tasks);
// skip all whitespace unless it contains a break which also breaks collection
INode sibling = x.getNextSibling();
while(sibling != null && sibling.getGrammarElement() == wsRule) {
if(sibling.getText().contains("\n")) {
commentSequence.clear();
continue COLLECT_LOOP;
}
sibling = sibling.getNextSibling();
}
if(sibling == null) {
commentSequence.clear();
continue;
}
// if adding a ML comment, use only the last, if adding a SL drop a preceding ML rule
if(commentSequence.size() > 0) {
if(grammarElement == mlRule)
commentSequence.clear();
else if(grammarElement == slRule &&
commentSequence.get(commentSequence.size() - 1).getGrammarElement() == mlRule)
commentSequence.clear();
}
commentSequence.add(x);
// if comment has anything but whitespace before its start (on same line), it is not a documentation comment
if(hasNonWSBeforeStart(root.getText(), x)) {
commentSequence.clear();
continue;
}
// if next is not a comment, it may be an element that the documentation should be associated with,
// but keep collecting if next is a comment
EObject siblingElement = sibling.getGrammarElement();
if(siblingElement == ga.getSL_COMMENTRule() || siblingElement == ga.getML_COMMENTRule())
continue; // keep on collecting
EObject semantic = NodeModelUtils.findActualSemanticObjectFor(sibling);
// check that a comment inside a structure (i.e. starts after the start of the structure)
// is not mistaken as following
EObject commentSemantic = NodeModelUtils.findActualSemanticObjectFor(x);
if(commentSemantic == semantic &&
x.getOffset() > NodeModelUtils.findActualNodeFor(semantic).getOffset())
continue;
found: {
for(Class<?> clazz : documentable) {
if(clazz.isAssignableFrom(semantic.getClass())) {
// found sequence is documentation for semantic
associateDocumentation(semantic, commentSequence);
// need a new sequence, or the one just given away may be cleared
commentSequence = Lists.newArrayList();
break found;
}
}
// next was not the right kind of element
commentSequence.clear();
}
}
}
associateTasks(model, tasks);
}
private void processCommentNode(INode node, List<PPTask> taskList) {
final String commentText = node.getText();
final String loweredText = commentText.toLowerCase();
if(commentText == null || commentText.length() == 0)
return;
int line = node.getStartLine();
for(int startPos = 0; startPos < commentText.length(); /* repeat value in loop */) {
int firstTagIndex = commentText.length();
int previousTagIndex = 0;
int firstTagLength = 0;
for(String tag : getTaskTags()) {
int idx = loweredText.indexOf(tag, startPos);
if(idx >= 0 && idx < firstTagIndex) {
firstTagIndex = idx;
firstTagLength = tag.length();
}
}
if(firstTagIndex == commentText.length())
return; // no tags
// msg is text to end, or up to (but not including) the newline.
int endIndex = commentText.indexOf("\n", firstTagIndex);
String msg = commentText.substring(firstTagIndex, endIndex < 0
? commentText.length()
: endIndex);
// trim ending */ from message if ML comment?
msg = msg.trim(); // remove trailing spaces (there can be to leading)
EObject ge = node.getGrammarElement();
if(ge instanceof TerminalRule) {
if("ML_COMMENT".equals(((TerminalRule) ge).getName())) {
if(msg.endsWith("*/")) {
msg = msg.substring(0, msg.length() - 2);
msg = msg.trim();
}
}
}
// if message is empty, or only contains ! take text before
// unless, ML comment where no newline was found in msg, take text both before and after
//
String taskMsg = msg;
boolean isImportant = msg.contains("!");
String checkForBang = isImportant
? msg.replace('!', ' ').trim()
: msg;
if(checkForBang.length() <= firstTagLength) {
// System.out.println("EMPTY TODO MSG");
// scan backwards from tag start
String allText = node.getRootNode().getText();
int offset = node.getOffset() + firstTagIndex + firstTagLength - 1;
StringBuilder builder = new StringBuilder();
for(int o = offset; o >= 0; o--) {
char c = allText.charAt(o);
if(c == '\n')
break;
builder.append(c);
}
builder.reverse();
taskMsg = builder.toString();
taskMsg = taskMsg.trim();
}
// increase lines seen by those that were passed between previous found and this.
for(int i = previousTagIndex; i < firstTagIndex; i++)
if(commentText.charAt(i) == '\n')
line++;
PPTask task = new PPTask(taskMsg, line, node.getOffset() + firstTagIndex, msg.length(), isImportant);
taskList.add(task);
if(endIndex < 0)
return; // done, no more tags could be found
startPos = endIndex + 1;
previousTagIndex = firstTagIndex;
}
}
/**
* Validate the state of documentation in model.
* TODO: Implement this (currently does nothing).
*
* @param model
* @param acceptor
*/
public void validateDocumentation(EObject model, IMessageAcceptor acceptor) {
}
}