/* * 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(); } } } }