/*
* Copyright 2010 the original author or authors.
*
* 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 org.gradle.build.docs.dsl.source;
import groovyjarjarantlr.collections.AST;
import org.apache.commons.lang.StringUtils;
import org.codehaus.groovy.antlr.GroovySourceAST;
import org.codehaus.groovy.antlr.LineColumn;
import org.codehaus.groovy.antlr.SourceBuffer;
import org.codehaus.groovy.antlr.treewalker.VisitorAdapter;
import org.gradle.build.docs.dsl.source.model.*;
import org.gradle.build.docs.dsl.source.model.ClassMetaData.MetaType;
import org.gradle.build.docs.model.ClassMetaDataRepository;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.codehaus.groovy.antlr.parser.GroovyTokenTypes.*;
public class SourceMetaDataVisitor extends VisitorAdapter {
private static final Pattern PREV_JAVADOC_COMMENT_PATTERN = Pattern.compile("(?s)/\\*\\*(.*?)\\*/");
private static final Pattern GETTER_METHOD_NAME = Pattern.compile("(get|is)(.+)");
private static final Pattern SETTER_METHOD_NAME = Pattern.compile("set(.+)");
private final SourceBuffer sourceBuffer;
private final LinkedList<GroovySourceAST> parseStack = new LinkedList<GroovySourceAST>();
private final List<String> imports = new ArrayList<String>();
private final ClassMetaDataRepository<ClassMetaData> repository;
private final List<ClassMetaData> allClasses = new ArrayList<ClassMetaData>();
private final LinkedList<ClassMetaData> classStack = new LinkedList<ClassMetaData>();
private final Map<GroovySourceAST, ClassMetaData> typeTokens = new HashMap<GroovySourceAST, ClassMetaData>();
private final boolean groovy;
private String packageName;
private LineColumn lastLineCol;
SourceMetaDataVisitor(SourceBuffer sourceBuffer, ClassMetaDataRepository<ClassMetaData> repository,
boolean isGroovy) {
this.sourceBuffer = sourceBuffer;
this.repository = repository;
groovy = isGroovy;
lastLineCol = new LineColumn(1, 1);
}
public void complete() {
for (String anImport : imports) {
for (ClassMetaData classMetaData : allClasses) {
classMetaData.addImport(anImport);
}
}
}
@Override
public void visitPackageDef(GroovySourceAST t, int visit) {
if (visit == OPENING_VISIT) {
packageName = extractName(t);
}
}
@Override
public void visitImport(GroovySourceAST t, int visit) {
if (visit == OPENING_VISIT) {
imports.add(extractName(t));
}
}
@Override
public void visitClassDef(GroovySourceAST t, int visit) {
visitTypeDef(t, visit, MetaType.CLASS);
}
@Override
public void visitInterfaceDef(GroovySourceAST t, int visit) {
visitTypeDef(t, visit, MetaType.INTERFACE);
}
@Override
public void visitEnumDef(GroovySourceAST t, int visit) {
visitTypeDef(t, visit, MetaType.ENUM);
}
@Override
public void visitAnnotationDef(GroovySourceAST t, int visit) {
visitTypeDef(t, visit, MetaType.ANNOTATION);
}
private void visitTypeDef(GroovySourceAST t, int visit, ClassMetaData.MetaType metaType) {
if (visit == OPENING_VISIT) {
ClassMetaData outerClass = getCurrentClass();
String baseName = extractIdent(t);
String className = outerClass != null ? outerClass.getClassName() + '.' + baseName
: packageName + '.' + baseName;
String comment = getJavaDocCommentsBeforeNode(t);
ClassMetaData currentClass = new ClassMetaData(className, packageName, metaType, groovy, comment);
if (outerClass != null) {
outerClass.addInnerClassName(className);
currentClass.setOuterClassName(outerClass.getClassName());
}
findAnnotations(t, currentClass);
classStack.addFirst(currentClass);
allClasses.add(currentClass);
typeTokens.put(t, currentClass);
repository.put(className, currentClass);
}
}
private ClassMetaData getCurrentClass() {
return classStack.isEmpty() ? null : classStack.getFirst();
}
@Override
public void visitExtendsClause(GroovySourceAST t, int visit) {
if (visit == OPENING_VISIT) {
ClassMetaData currentClass = getCurrentClass();
for (
GroovySourceAST child = (GroovySourceAST) t.getFirstChild(); child != null;
child = (GroovySourceAST) child.getNextSibling()) {
if (!currentClass.isInterface()) {
currentClass.setSuperClassName(extractName(child));
} else {
currentClass.addInterfaceName(extractName(child));
}
}
}
}
@Override
public void visitImplementsClause(GroovySourceAST t, int visit) {
if (visit == OPENING_VISIT) {
ClassMetaData currentClass = getCurrentClass();
for (
GroovySourceAST child = (GroovySourceAST) t.getFirstChild(); child != null;
child = (GroovySourceAST) child.getNextSibling()) {
currentClass.addInterfaceName(extractName(child));
}
}
}
@Override
public void visitEnumConstantDef(GroovySourceAST t, int visit) {
if (visit == OPENING_VISIT) {
String name = extractName(t);
getCurrentClass().addEnumConstant(name);
skipJavaDocComment(t);
}
}
@Override
public void visitMethodDef(GroovySourceAST t, int visit) {
if (visit == OPENING_VISIT) {
maybeAddMethod(t);
skipJavaDocComment(t);
}
}
private void maybeAddMethod(GroovySourceAST t) {
String name = extractName(t);
if (!groovy && name.equals(getCurrentClass().getSimpleName())) {
// A constructor. The java grammar treats a constructor as a method, the groovy grammar does not.
return;
}
ASTIterator children = new ASTIterator(t);
if (groovy) {
children.skip(TYPE_PARAMETERS);
children.skip(MODIFIERS);
} else {
children.skip(MODIFIERS);
children.skip(TYPE_PARAMETERS);
}
String rawCommentText = getJavaDocCommentsBeforeNode(t);
TypeMetaData returnType = extractTypeName(children.current);
MethodMetaData method = getCurrentClass().addMethod(name, returnType, rawCommentText);
findAnnotations(t, method);
extractParameters(t, method);
Matcher matcher = GETTER_METHOD_NAME.matcher(name);
if (matcher.matches()) {
int startName = matcher.start(2);
String propName = name.substring(startName, startName + 1).toLowerCase() + name.substring(startName + 1);
PropertyMetaData property = getCurrentClass().addReadableProperty(propName, returnType, rawCommentText, method);
for (String annotation : method.getAnnotationTypeNames()) {
property.addAnnotationTypeName(annotation);
}
return;
}
if (method.getParameters().size() != 1) {
return;
}
matcher = SETTER_METHOD_NAME.matcher(name);
if (matcher.matches()) {
int startName = matcher.start(1);
String propName = name.substring(startName, startName + 1).toLowerCase() + name.substring(startName + 1);
TypeMetaData type = method.getParameters().get(0).getType();
getCurrentClass().addWriteableProperty(propName, type, rawCommentText, method);
}
}
private void extractParameters(GroovySourceAST t, MethodMetaData method) {
GroovySourceAST paramsAst = t.childOfType(PARAMETERS);
for (
GroovySourceAST child = (GroovySourceAST) paramsAst.getFirstChild(); child != null;
child = (GroovySourceAST) child.getNextSibling()) {
assert child.getType() == PARAMETER_DEF || child.getType() == VARIABLE_PARAMETER_DEF;
TypeMetaData type = extractTypeName((GroovySourceAST) child.getFirstChild().getNextSibling());
if (child.getType() == VARIABLE_PARAMETER_DEF) {
type.setVarargs();
}
method.addParameter(extractIdent(child), type);
}
}
@Override
public void visitVariableDef(GroovySourceAST t, int visit) {
if (visit == OPENING_VISIT) {
maybeAddPropertyFromField(t);
skipJavaDocComment(t);
}
}
private void maybeAddPropertyFromField(GroovySourceAST t) {
GroovySourceAST parentNode = getParentNode();
boolean isField = parentNode != null && parentNode.getType() == OBJBLOCK;
if (!isField) {
return;
}
int modifiers = extractModifiers(t);
boolean isConst = getCurrentClass().isInterface() || (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers));
if (isConst) {
visitConst(t);
return;
}
boolean isProp = groovy && !Modifier.isStatic(modifiers) && !Modifier.isPublic(modifiers)
&& !Modifier.isProtected(modifiers) && !Modifier.isPrivate(modifiers);
if (!isProp) {
return;
}
ASTIterator children = new ASTIterator(t);
children.skip(MODIFIERS);
String propertyName = extractIdent(t);
TypeMetaData propertyType = extractTypeName(children.current);
ClassMetaData currentClass = getCurrentClass();
MethodMetaData getterMethod = currentClass.addMethod(String.format("get%s", StringUtils.capitalize(
propertyName)), propertyType, "");
PropertyMetaData property = currentClass.addReadableProperty(propertyName, propertyType, getJavaDocCommentsBeforeNode(t), getterMethod);
findAnnotations(t, property);
if (!Modifier.isFinal(modifiers)) {
MethodMetaData setterMethod = currentClass.addMethod(String.format("set%s", StringUtils.capitalize(
propertyName)), TypeMetaData.VOID, "");
setterMethod.addParameter(propertyName, propertyType);
currentClass.addWriteableProperty(propertyName, propertyType, getJavaDocCommentsBeforeNode(t), setterMethod);
}
}
private void visitConst(GroovySourceAST t) {
String constName = extractIdent(t);
GroovySourceAST assign = t.childOfType(ASSIGN);
String value = null;
if (assign != null) {
value = extractLiteral(assign.getFirstChild());
}
getCurrentClass().getConstants().put(constName, value);
}
private String extractLiteral(AST ast) {
switch (ast.getType()) {
case EXPR:
// The java grammar wraps initialisers in an EXPR token
return extractLiteral(ast.getFirstChild());
case NUM_INT:
case NUM_LONG:
case NUM_FLOAT:
case NUM_DOUBLE:
case NUM_BIG_INT:
case NUM_BIG_DECIMAL:
case STRING_LITERAL:
return ast.getText();
}
return null;
}
public GroovySourceAST pop() {
if (!parseStack.isEmpty()) {
GroovySourceAST ast = parseStack.removeFirst();
ClassMetaData classMetaData = typeTokens.remove(ast);
if (classMetaData != null) {
assert classMetaData == classStack.getFirst();
classStack.removeFirst();
}
return ast;
}
return null;
}
@Override
public void push(GroovySourceAST t) {
parseStack.addFirst(t);
}
private GroovySourceAST getParentNode() {
if (parseStack.size() > 1) {
return parseStack.get(1);
}
return null;
}
private int extractModifiers(GroovySourceAST ast) {
GroovySourceAST modifiers = ast.childOfType(MODIFIERS);
if (modifiers == null) {
return 0;
}
int modifierFlags = 0;
for (
GroovySourceAST child = (GroovySourceAST) modifiers.getFirstChild(); child != null;
child = (GroovySourceAST) child.getNextSibling()) {
switch (child.getType()) {
case LITERAL_private:
modifierFlags |= Modifier.PRIVATE;
break;
case LITERAL_protected:
modifierFlags |= Modifier.PROTECTED;
break;
case LITERAL_public:
modifierFlags |= Modifier.PUBLIC;
break;
case FINAL:
modifierFlags |= Modifier.FINAL;
break;
case LITERAL_static:
modifierFlags |= Modifier.STATIC;
break;
}
}
return modifierFlags;
}
private TypeMetaData extractTypeName(GroovySourceAST ast) {
TypeMetaData type = new TypeMetaData();
switch (ast.getType()) {
case TYPE:
GroovySourceAST typeName = (GroovySourceAST) ast.getFirstChild();
extractTypeName(typeName, type);
break;
case WILDCARD_TYPE:
// In the groovy grammar, the bounds are sibling of the ?, in the java grammar, they are the child
GroovySourceAST bounds = (GroovySourceAST) (groovy ? ast.getNextSibling() : ast.getFirstChild());
if (bounds == null) {
type.setWildcard();
} else if (bounds.getType() == TYPE_UPPER_BOUNDS) {
type.setUpperBounds(extractTypeName((GroovySourceAST) bounds.getFirstChild()));
} else if (bounds.getType() == TYPE_LOWER_BOUNDS) {
type.setLowerBounds(extractTypeName((GroovySourceAST) bounds.getFirstChild()));
}
break;
case IDENT:
case DOT:
extractTypeName(ast, type);
break;
default:
throw new RuntimeException(String.format("Unexpected token in type name: %s", ast));
}
return type;
}
private void extractTypeName(GroovySourceAST ast, TypeMetaData type) {
if (ast == null) {
type.setName("java.lang.Object");
return;
}
switch (ast.getType()) {
case LITERAL_boolean:
type.setName("boolean");
return;
case LITERAL_byte:
type.setName("byte");
return;
case LITERAL_char:
type.setName("char");
return;
case LITERAL_double:
type.setName("double");
return;
case LITERAL_float:
type.setName("float");
return;
case LITERAL_int:
type.setName("int");
return;
case LITERAL_long:
type.setName("long");
return;
case LITERAL_void:
type.setName("void");
return;
case ARRAY_DECLARATOR:
extractTypeName((GroovySourceAST) ast.getFirstChild(), type);
type.addArrayDimension();
return;
}
type.setName(extractName(ast));
GroovySourceAST typeArgs = ast.childOfType(TYPE_ARGUMENTS);
if (typeArgs != null) {
for (
GroovySourceAST child = (GroovySourceAST) typeArgs.getFirstChild(); child != null;
child = (GroovySourceAST) child.getNextSibling()) {
assert child.getType() == TYPE_ARGUMENT;
type.addTypeArg(extractTypeName((GroovySourceAST) child.getFirstChild()));
}
}
}
private void skipJavaDocComment(GroovySourceAST t) {
lastLineCol = new LineColumn(t.getLine(), t.getColumn());
}
private String getJavaDocCommentsBeforeNode(GroovySourceAST t) {
String result = "";
LineColumn thisLineCol = new LineColumn(t.getLine(), t.getColumn());
String text = sourceBuffer.getSnippet(lastLineCol, thisLineCol);
if (text != null) {
Matcher m = PREV_JAVADOC_COMMENT_PATTERN.matcher(text);
if (m.find()) {
result = m.group(1);
}
}
lastLineCol = thisLineCol;
return result;
}
private void findAnnotations(GroovySourceAST t, AbstractLanguageElement currentElement) {
GroovySourceAST modifiers = t.childOfType(MODIFIERS);
if (modifiers != null) {
List<GroovySourceAST> children = modifiers.childrenOfType(ANNOTATION);
for (GroovySourceAST child : children) {
String identifier = extractIdent(child);
currentElement.addAnnotationTypeName(identifier);
}
}
}
private String extractIdent(GroovySourceAST t) {
return t.childOfType(IDENT).getText();
}
private String extractName(GroovySourceAST t) {
if (t.getType() == DOT) {
GroovySourceAST firstChild = (GroovySourceAST) t.getFirstChild();
GroovySourceAST secondChild = (GroovySourceAST) firstChild.getNextSibling();
return extractName(firstChild) + "." + extractName(secondChild);
}
if (t.getType() == IDENT) {
return t.getText();
}
if (t.getType() == STAR) {
return t.getText();
}
GroovySourceAST child = t.childOfType(DOT);
if (child != null) {
return extractName(child);
}
child = t.childOfType(IDENT);
if (child != null) {
return extractName(child);
}
throw new RuntimeException(String.format("Unexpected token in name: %s", t));
}
private static class ASTIterator {
GroovySourceAST current;
private ASTIterator(GroovySourceAST parent) {
this.current = (GroovySourceAST) parent.getFirstChild();
}
void skip(int token) {
if (current != null && current.getType() == token) {
current = (GroovySourceAST) current.getNextSibling();
}
}
}
}