blob: 819742382199828c39435f7ac0c1181e7e5d8d8c [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016 Chalmers | University of Gothenburg, rt-labs and others.
* 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:
* Chalmers | University of Gothenburg and rt-labs - initial API and implementation and/or initial documentation
*******************************************************************************/
package org.eclipse.capra.handler.cdt;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.capra.handler.cdt.preferences.CDTPreferences;
import org.eclipse.cdt.core.dom.ast.IASTComment;
import org.eclipse.cdt.core.dom.ast.IASTCompositeTypeSpecifier;
import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IASTSimpleDeclaration;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.cdt.core.dom.rewrite.ASTRewrite.CommentPosition;
import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.IBuffer;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ISourceRange;
import org.eclipse.cdt.core.model.ISourceReference;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
/**
* Code for updating requirement annotation tag in Doxygen comments. Gets
* comments from the CDT element, matches out the requirement tag and replaces
* it with the new annotation.
*
* TODO: This code doesn't work for the declaration of C++ class members. For
* some reason comments on C++ members are parsed as trailing to previous
* element instead of leading the one we want it to be associated with. Because
* of that we don't find them.
*/
public class CDTAnnotate {
/**
* Creates or replaces the annotation on the handle element.
*
* @param handle The CDT object that contains the source element where the tag annotation comment should be placed.
* @param annotation The artifact annotation text (not including the tag)
* @throws CoreException If an error occur while working with the C model. Potential errors include: IO error while
* reading/writing to the file; the argument handle element has an unexpected structure which cannot be handled.
*/
public static void annotateArtifact(ICElement handle, String annotation) throws CoreException {
if (!(handle instanceof ISourceReference)) return;
ISourceReference sourceRef = (ISourceReference) handle;
IEclipsePreferences preferences = CDTPreferences.getPreferences();
String tag = preferences.get(
CDTPreferences.ANNOTATE_CDT_TAG_PREFIX, CDTPreferences.ANNOTATE_CDT_TAG_PREFIX_DEFAULT).trim()
+ preferences.get(CDTPreferences.ANNOTATE_CDT_TAG, CDTPreferences.ANNOTATE_CDT_TAG_DEFAULT).trim();
if (sourceRef instanceof ITranslationUnit) {
annotateFile(annotation, tag, (ITranslationUnit) sourceRef);
} else {
annotateDeclaration(annotation, tag, sourceRef);
}
}
private static void annotateFile(String annotation, String tag, ITranslationUnit translationUnit) throws CoreException {
Optional<IASTComment> oldCommentNode = Arrays.stream(translationUnit.getAST().getComments())
.filter(c -> isDoxygenFileComment(c.getRawSignature()))
.findFirst();
String newline = findFirstNewline(translationUnit.getBuffer());
String newCommentText = createNewCommentString(
oldCommentNode.map(c -> c.getRawSignature()),
annotation, tag, newline, true);
if (oldCommentNode.isPresent()) {
IASTFileLocation loc = oldCommentNode.orElse(null).getFileLocation();
translationUnit.getBuffer().replace(loc.getNodeOffset(), loc.getNodeLength(), newCommentText);
} else {
// Put some space after new comment
translationUnit.getBuffer().replace(0, 0, newCommentText + newline);
}
translationUnit.getBuffer().save(new NullProgressMonitor(), false);
}
private static Pattern FILE_TAG_PATTERN = Pattern.compile(""
+ "^((/\\*[!*])" // Start of Doxygen comment
+ "|"
+ "(\\s*\\*?))" // Comment body: Optionally leading star and spaces
+ "\\s*"
+ "[@\\\\]file\\W" // Tag: Either \file or @file, with non-word char afterwards
, Pattern.MULTILINE);
static boolean isDoxygenFileComment(String comment) {
return comment != null && FILE_TAG_PATTERN.matcher(comment).find();
}
private static void annotateDeclaration(String annotation, String tag, ISourceReference sourceRef)
throws CoreException, CModelException
{
ITranslationUnit translationUnit = sourceRef.getTranslationUnit();
IASTTranslationUnit ast = translationUnit.getAST();
IASTNode node = findNode(sourceRef, ast);
String newline = findNewlineAndIndentationBefore(translationUnit.getBuffer(), node.getFileLocation().getNodeOffset());
// Use ASTRewrite just to get comments for node
ASTRewrite rewrite = ASTRewrite.create(ast);
Optional<IASTComment> oldCommentNode = getDoxygenComment(rewrite.getComments(node, CommentPosition.leading));
String newCommentText = createNewCommentString(
oldCommentNode.map(c -> c.getRawSignature()),
annotation, tag, newline, false);
if (oldCommentNode.isPresent()) {
IASTFileLocation loc = oldCommentNode.get().getFileLocation();
translationUnit.getBuffer().replace(loc.getNodeOffset(), loc.getNodeLength(), newCommentText);
} else {
translationUnit.getBuffer().replace(node.getFileLocation().getNodeOffset(), 0, newCommentText);
}
translationUnit.getBuffer().save(new NullProgressMonitor(), false);
}
private static IASTNode findNode(ISourceReference sourceRef, IASTTranslationUnit ast) throws CModelException {
ISourceRange sourceRange = sourceRef.getSourceRange();
IASTNode node = ast.getNodeSelector(null)
.findEnclosingNode(sourceRange.getStartPos(), sourceRange.getLength());
// If this is a struct with a typedef then the annotation should go on the typedef instead
if (node instanceof IASTCompositeTypeSpecifier) {
IASTNode parent = ((IASTCompositeTypeSpecifier) node).getParent();
if (parent instanceof IASTSimpleDeclaration) {
return parent;
}
}
return node;
}
/**
* @return A new comment, either with the tag section in comment substituted by the content of
* annotation, or newly constructed comment with annotation as its content.
*/
// Package visibility for testing
static String createNewCommentString(Optional<String> oldCom, String annotation, String tag, String nl, boolean addFileTag) {
if (!oldCom.isPresent()) {
// There were no previous comment, create a new one
String newComment = "/**" + nl;
if (addFileTag) newComment += " * @file" + nl;
newComment += " * " + tag + " " + annotation + nl
+ " */" + nl;
return newComment;
}
String oldComment = oldCom.get();
int tagIx = oldComment.indexOf(tag);
if (tagIx != -1) {
// There is an old tag. Match it and all of its annotation and replace with new content.
// Match the first thing after the tag and the annotations
Matcher endOfTagMatcher = Pattern.compile(
// Empty line, with star and optional spaces
"(\r?\n\\s*\\*\\s*\r?\n)"
+ "|"
// Comment end, including leading space and optional newline
+ "(\\s*(\r?\n)?\\s*\\*/(\r?\n)?)")
.matcher(oldComment);
if (endOfTagMatcher.find(tagIx + tag.length())) {
String beforeTagText = oldComment.substring(0, tagIx);
String afterTagText = oldComment.substring(endOfTagMatcher.start());
return beforeTagText + tag + " " + annotation + afterTagText;
}
} else {
// There is no tag. Insert tag in the end of the comment.
Matcher commentEndMatcher = Pattern.compile(
".*?" // The whole comment, except the end, non-greedy
+ "(" // A group for all text that should be replaced
+ "(\r?\n)?" // Remove last newline (if there is one)
+ "\\s*\\*/)", // Leading spaces and end-of-comment
Pattern.DOTALL).matcher(oldComment);
if (commentEndMatcher.find()) {
return oldComment.substring(0, commentEndMatcher.start(1))
+ nl + " * " + tag + " " + annotation + nl + " */";
}
}
// This is a weird comment indeed. What to do?
throw new IllegalStateException("Weird comment: " + oldComment);
}
/**
* @return The first comment that is in Doxygen format (starts with /**).
*/
private static Optional<IASTComment> getDoxygenComment(List<IASTComment> comments) {
// Begging search from the comment nearest to the node
Collections.reverse(comments);
return comments.stream()
.filter(c -> isDoxygenComment(c))
.filter(c -> !isDoxygenFileComment(c.getRawSignature()))
.findAny();
}
private static boolean isDoxygenComment(IASTComment comment) {
String text = comment.getRawSignature();
return text.startsWith("/**") || text.startsWith("/*!");
}
/**
* Returns the newline chars of the first line in text.
*/
private static String findFirstNewline(IBuffer text) {
for (int charIx = 0; charIx < text.getLength(); charIx++) {
String newline = getNewline(text, charIx);
if (newline != null) return newline;
}
// The argument text has no newlines, default to \n
return "\n";
}
private static String getNewline(IBuffer text, int offset) {
if (text.getChar(offset) == '\n') {
if (offset > 0 && text.getChar(offset - 1) == '\r') return "\r\n";
else return "\n";
} else if (text.getChar(offset) == '\r') {
return "\r";
} else {
return null;
}
}
private static final Pattern NON_SPACES_PATTERN = Pattern.compile("\\S");
/**
* Returns the newline and indentation chars for the line on offset.
*/
private static String findNewlineAndIndentationBefore(IBuffer text, int offset) {
for (int charIx = offset; charIx >= 0; charIx--) {
String newline = getNewline(text, charIx);
if (newline != null) {
// Get text from end of newline, to end of indentation (first non-space),
String indentation = text.getText(charIx + 1, offset - charIx - 1);
// Even if offset was for something with other char before it, we indent using
// only whitespace
return newline + NON_SPACES_PATTERN.matcher(indentation).replaceAll(" ");
}
}
return findFirstNewline(text);
}
}