com.google.devtools.javatools.transform.scrubber.StrippingRangeRecorder.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.javatools.transform.scrubber.StrippingRangeRecorder.java

Source

/*
 * Copyright 2012 Google Inc. All Rights Reserved.
 *
 * 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.devtools.javatools.transform.scrubber;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeSet;

import com.sun.source.tree.LineMap;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.tree.JCTree.JCModifiers;
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
import com.sun.tools.javac.tree.TreeScanner;

import java.util.Set;
import java.util.regex.Pattern;

/**
 * Scanner of the compilation unit tree that records the position ranges to
 * remove for stripping annotations.  The removal is line-based: a line is either completely
 * retained or completely removed.
 */
class StrippingRangeRecorder extends TreeScanner {

    private final CharSequence fileContent;
    private final Set<String> stripAnnotationClasses;
    private final Set<String> includeAnnotationClasses;
    private final RangeSet<Integer> rangesToRemove = TreeRangeSet.create();

    private JCCompilationUnit compilationUnit;
    private LineMap lineMap;
    private int lastLine;
    /**
     * Whether code traversal is currently inside an included class. We need this to determine whether
     * to include or exclude an inner class.
     */
    private boolean insideClass = false;

    StrippingRangeRecorder(CharSequence fileContent, Set<String> stripAnnotations, Set<String> includeAnnotations) {
        this.fileContent = checkNotNull(fileContent, "fileContent");
        this.stripAnnotationClasses = checkNotNull(stripAnnotations, "stripAnnotations");
        this.includeAnnotationClasses = checkNotNull(includeAnnotations, "includeAnnotations");
    }

    @Override
    public void visitTopLevel(JCCompilationUnit compilationUnit) {
        this.compilationUnit = checkNotNull(compilationUnit, "compilationUnit");
        this.lineMap = compilationUnit.getLineMap();
        this.lastLine = (int) lineMap.getLineNumber(fileContent.length() - 1);
        super.visitTopLevel(this.compilationUnit);
    }

    @Override
    public void visitMethodDef(JCMethodDecl method) {
        if (!stripElement(method, method.getModifiers())) {
            super.visitMethodDef(method);
        }
    }

    @Override
    public void visitClassDef(JCClassDecl clazz) {
        if (!stripElement(clazz, clazz.getModifiers())) {
            boolean oldInsideClass = insideClass;
            insideClass = true;
            super.visitClassDef(clazz);
            insideClass = oldInsideClass;
        }
    }

    @Override
    public void visitVarDef(JCVariableDecl var) {
        if (!stripElement(var, var.getModifiers())) {
            super.visitVarDef(var);
        }
    }

    RangeSet<Integer> getRangesToRemove() {
        return rangesToRemove;
    }

    private void removeElement(JCTree tree) {
        int startPos = tree.getStartPosition();
        /*
         * The end position of this element, exclusive.  Move to the beginning
         * of the next line if possible.
         */
        int endPos = tree.getEndPosition(compilationUnit.endPositions);
        final int endPosLineNumber = (int) lineMap.getLineNumber(endPos);
        if (endPosLineNumber < lastLine) {
            endPos = (int) lineMap.getStartPosition(endPosLineNumber + 1);
        }

        /*
         * If this element has javadoc, backtrack and skip it.  The javadoc is
         * identified by a regex derived from the content.
         */
        if (compilationUnit.docComments.containsKey(tree)) {
            startPos = backtrackForJavadoc(tree, startPos);
        }
        /*
         * Strip the leading blank lines as well.
         */
        startPos = backtraceForLeadingWhitespace(startPos);
        /*
         * Move the startingPos to the beginning of line to avoid trailing whitespace
         * of the previous remaining program elements.
         */
        startPos = (int) lineMap.getStartPosition(lineMap.getLineNumber(startPos));
        rangesToRemove.add(Range.closedOpen(startPos, endPos));
    }

    private int backtrackForJavadoc(JCTree tree, int startPos) {
        final String docComment = compilationUnit.docComments.get(tree);
        final StringBuffer docRegex = new StringBuffer(".*" + Pattern.quote("/**"));
        for (String line : Splitter.on("\n").split(docComment)) {
            docRegex.append("[\\s]*") // each line can optionally start with whitespaces.
                    /*
                     * The starting '*' is optional.  javadoc doesn't require each
                     * line to with '*'.  One-liner javadoc also doesn't have the
                     * leading '*'.
                     */
                    .append(Pattern.quote("*") + "?").append(Pattern.quote(line));
        }
        docRegex
                /*
                 * The final * is optional.  Single-line javadoc needs it, but
                 * multiline javadoc doesn't have it (the '*' on the last line accounts
                 * for the '*'.
                 */
                .append(Pattern.quote("*") + "?").append(Pattern.quote("/")).append(".*"); // in case there are other comments (line or block) after this.
        final Pattern docPattern = Pattern.compile(docRegex.toString(), Pattern.MULTILINE | Pattern.DOTALL);

        int commentStartPos = (int) lineMap.getStartPosition(lineMap.getLineNumber(startPos));
        boolean matched = docPattern.matcher(fileContent.subSequence(commentStartPos, startPos)).matches();
        while (!matched && commentStartPos > 0) {
            /*
             * Keep going back, one line at a time, until we match the full javadoc.
             * The 'commentStartPos > 0' sholdn't be necessary unless we have a bug.
             */
            commentStartPos = (int) lineMap.getStartPosition(lineMap.getLineNumber(commentStartPos) - 1);
            matched = docPattern.matcher(fileContent.subSequence(commentStartPos, startPos)).matches();
        }
        checkState(matched, "Cannot find the javadoc [%s] for [%s] (pattern [%s])", docComment, tree,
                docPattern.pattern());
        return commentStartPos;
    }

    private int backtraceForLeadingWhitespace(int startPos) {
        int whiteSpaceLinesStartPos = startPos;
        boolean isWhitespaceLine = true;
        while (isWhitespaceLine && whiteSpaceLinesStartPos > 0) {
            int previousLineStart = (int) lineMap
                    .getStartPosition(lineMap.getLineNumber(whiteSpaceLinesStartPos) - 1);
            /*
             * In principle, we should use the JLS definition of whitespace from
             * http://java.sun.com/docs/books/jls/third_edition/html/lexical.html#3.6 ... but we already
             * play fast and loose with the spec by requiring lines to be split by \n, by using regex \s
             * elsewhere for whitespace, and probably other stuff, and things work just fine.
             */
            isWhitespaceLine = CharMatcher.WHITESPACE
                    .matchesAllOf(fileContent.subSequence(previousLineStart, whiteSpaceLinesStartPos));

            if (isWhitespaceLine) {
                whiteSpaceLinesStartPos = previousLineStart;
            }
        }
        return whiteSpaceLinesStartPos;
    }

    /**
     * Strips the given element from source if needed. Also strips its include annotations.
     *
     * @return whether the element itself was stripped
     */
    private boolean stripElement(JCTree element, JCModifiers modifiers) {
        for (JCAnnotation a : modifiers.getAnnotations()) {
            String annotationName = a.getAnnotationType().toString().trim();
            if (stripAnnotationClasses.contains(annotationName)) {
                removeElement(element);
                return true;
            } else if (includeAnnotationClasses.contains(annotationName)) {
                removeElement(a);
                return false;
            }
        }
        // Strip an un-annotated element iff it's an unincluded outer class.
        boolean isClass = (element.getKind() == Tree.Kind.CLASS);
        if (isClass && !insideClass && !includeAnnotationClasses.isEmpty()) {
            removeElement(element);
            return true;
        }
        return false;
    }
}