lombok.ast.grammar.Source.java Source code

Java tutorial

Introduction

Here is the source code for lombok.ast.grammar.Source.java

Source

/*
 * Copyright (C) 2010 The Project Lombok Authors.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package lombok.ast.grammar;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.TreeMap;

import lombok.Getter;
import lombok.ast.Comment;
import lombok.ast.Expression;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.JavadocContainer;
import lombok.ast.Node;
import lombok.ast.Position;

import org.parboiled.Context;
import org.parboiled.RecoveringParseRunner;
import org.parboiled.errors.ParseError;
import org.parboiled.support.ParsingResult;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Maps;

public class Source {
    @Getter
    private final String name;
    @Getter
    private final String rawInput;
    private List<Node> nodes;
    private List<ParseProblem> problems;
    private List<Comment> comments;
    private boolean parsed;
    private ParsingResult<Node> parsingResult;

    private TreeMap<Integer, Integer> positionDeltas;
    private Map<org.parboiled.Node<Node>, Node> registeredStructures;
    private Map<org.parboiled.Node<Node>, List<Comment>> registeredComments;
    private String preprocessed;
    private Map<Node, Collection<SourceStructure>> cachedSourceStructures;
    private List<Integer> lineEndings;

    public Source(String rawInput, String name) {
        this.rawInput = rawInput;
        this.name = name;
        clear();
    }

    public List<Node> getNodes() {
        parseCompilationUnit();
        if (!parsed)
            throw new IllegalStateException("Code hasn't been parsed yet.");
        return nodes;
    }

    public List<ParseProblem> getProblems() {
        parseCompilationUnit();
        return problems;
    }

    public void clear() {
        nodes = Lists.newArrayList();
        problems = Lists.newArrayList();
        comments = Lists.newArrayList();
        lineEndings = ImmutableList.of();
        parsed = false;
        parsingResult = null;
        positionDeltas = Maps.newTreeMap();
        registeredComments = new MapMaker().weakKeys().makeMap();
        registeredStructures = new MapMaker().weakKeys().makeMap();
        cachedSourceStructures = null;
    }

    public String getOverviewProfileInformation() {
        clear();
        preProcess();
        ParserGroup group = new ParserGroup(this);
        ProfilerParseRunner<Node> runner = new ProfilerParseRunner<Node>(group.structures.compilationUnitEoi(),
                preprocessed);
        this.parsingResult = runner.run();
        StringBuilder out = new StringBuilder();
        out.append(runner.getOverviewReport());
        postProcess();
        return out.toString();
    }

    public List<String> getDetailedProfileInformation(int top) {
        clear();
        preProcess();
        ParserGroup group = new ParserGroup(this);
        ProfilerParseRunner<Node> runner = new ProfilerParseRunner<Node>(group.structures.compilationUnitEoi(),
                preprocessed);
        this.parsingResult = runner.run();
        List<String> result = Lists.newArrayList();
        result.add(runner.getOverviewReport());
        result.addAll(runner.getExtendedReport(top));
        postProcess();
        return result;
    }

    private List<Integer> calculateLineEndings() {
        ImmutableList.Builder<Integer> builder = ImmutableList.builder();

        boolean atCR = false;
        for (int i = 0; i < rawInput.length(); i++) {
            char c = rawInput.charAt(i);
            if (c == '\n' && !atCR)
                builder.add(i);
            atCR = c == '\r';
            if (atCR)
                builder.add(i);
        }
        return builder.build();
    }

    public void parseCompilationUnit() {
        if (parsed)
            return;
        preProcess();
        ParserGroup group = new ParserGroup(this);
        parsingResult = RecoveringParseRunner.run(group.structures.compilationUnitEoi(), preprocessed);
        postProcess();
    }

    public void parseMember() {
        if (parsed)
            return;
        preProcess();
        ParserGroup group = new ParserGroup(this);
        parsingResult = RecoveringParseRunner.run(group.structures.typeBodyMember(), preprocessed);
        postProcess();
    }

    public void parseStatement() {
        if (parsed)
            return;
        preProcess();
        ParserGroup group = new ParserGroup(this);
        parsingResult = RecoveringParseRunner.run(group.statements.anyStatement(), preprocessed);
        postProcess();
    }

    public void parseExpression() {
        if (parsed)
            return;
        preProcess();
        ParserGroup group = new ParserGroup(this);
        parsingResult = RecoveringParseRunner.run(group.expressions.anyExpression(), preprocessed);
        postProcess();
    }

    public void parseVariableDefinition() {
        if (parsed)
            return;
        preProcess();
        ParserGroup group = new ParserGroup(this);
        parsingResult = RecoveringParseRunner.run(group.structures.variableDefinition(), preprocessed);
        postProcess();
    }

    private void postProcess() {
        for (ParseError error : parsingResult.parseErrors) {
            int errStart = error.getStartIndex();
            int errEnd = error.getEndIndex();
            problems.add(
                    new ParseProblem(new Position(mapPosition(errStart), mapPosition(errEnd)), error.toString()));
        }

        if (parsingResult.parseTreeRoot != null) {
            nodes.add(parsingResult.parseTreeRoot.getValue());
            gatherComments(parsingResult.parseTreeRoot);
        }

        comments = Collections.unmodifiableList(comments);
        nodes = Collections.unmodifiableList(nodes);
        problems = Collections.unmodifiableList(problems);

        rtrimPositions(nodes, comments);

        //TODO Write test case with javadoc intermixed with empty declares.
        //TODO test javadoc on a package declaration.
        //TODO javadoc in between keywords.

        associateJavadoc(comments, nodes);

        fixPositions(nodes);
        fixPositions(comments);

        parsed = true;
    }

    void registerStructure(Node node, org.parboiled.Node<Node> pNode) {
        registeredStructures.put(pNode, node);
    }

    public Map<Node, Collection<SourceStructure>> getSourceStructures() {
        if (cachedSourceStructures != null)
            return cachedSourceStructures;
        parseCompilationUnit();
        ListMultimap<Node, SourceStructure> map = LinkedListMultimap.create();

        org.parboiled.Node<Node> pNode = parsingResult.parseTreeRoot;

        buildSourceStructures(pNode, null, map);

        Map<Node, Collection<SourceStructure>> result = map.asMap();

        for (Collection<SourceStructure> structures : result.values()) {
            for (SourceStructure structure : structures) {
                structure.setPosition(new Position(mapPosition(structure.getPosition().getStart()),
                        mapPosition(structure.getPosition().getEnd())));
            }
        }

        return cachedSourceStructures = result;
    }

    private void addSourceStructure(ListMultimap<Node, SourceStructure> map, Node node, SourceStructure structure) {
        if (structure.getPosition().size() > 0 && structure.getContent().trim().length() > 0
                && !structure.getPosition().equals(node.getPosition())) {

            map.put(node, structure);
        }
    }

    private void buildSourceStructures(org.parboiled.Node<Node> pNode, Node owner,
            ListMultimap<Node, SourceStructure> map) {
        Node target = registeredStructures.remove(pNode);
        if (target != null || pNode.getChildren().isEmpty()) {
            int start = pNode.getStartIndex();
            int end = pNode.getEndIndex();
            String text = preprocessed.substring(start, end);
            SourceStructure structure = new SourceStructure(new Position(start, end), text);
            if (target != null)
                addSourceStructure(map, target, structure);
            else if (pNode.getValue() != null && !(pNode.getValue() instanceof TemporaryNode))
                addSourceStructure(map, pNode.getValue(), structure);
            else if (owner != null)
                addSourceStructure(map, owner, structure);
        } else {
            Node possibleOwner = pNode.getValue();
            if (possibleOwner instanceof TemporaryNode)
                possibleOwner = null;
            for (org.parboiled.Node<Node> child : pNode.getChildren()) {
                if (child.getValue() == null || child.getValue() instanceof TemporaryNode)
                    continue;
                /* If the next if holds true, then we aren't the true generator; the child generated the node and this pNode adopted it */
                if (child.getValue() == possibleOwner)
                    possibleOwner = null;
            }

            if (possibleOwner != null)
                owner = possibleOwner;

            for (org.parboiled.Node<Node> child : pNode.getChildren()) {
                buildSourceStructures(child, owner, map);
            }
        }
    }

    /**
     * The end positions of all nodes include their trailing whitespace which isn't very convenient.
     * We'll 'fix' the end marker of each node by trimming it back. This is somewhat complicated as comments also need to be trimmed across.
     * We also adjust all positions to conform with the raw input (undoing any positional shifts caused by preprocessing).
     */
    private void rtrimPositions(List<Node> nodes, List<Comment> comments) {
        final boolean[] whitespace = new boolean[preprocessed.length()];
        for (Comment comment : comments) {
            Position p = comment.getPosition();
            if (!p.isUnplaced()) {
                for (int i = p.getStart(); i < p.getEnd(); i++)
                    whitespace[i] = true;
            }
        }

        /* Process actual whitespace in preprocessed source data */ {
            char[] chars = preprocessed.toCharArray();
            for (int i = 0; i < chars.length; i++)
                if (Character.isWhitespace(chars[i]))
                    whitespace[i] = true;
        }

        for (Node node : nodes)
            node.accept(new ForwardingAstVisitor() {
                @Override
                public boolean visitNode(Node node) {
                    Position p = node.getPosition();
                    if (p.isUnplaced())
                        return false;

                    int trimmed = Math.min(whitespace.length, p.getEnd());
                    while (trimmed > 0 && whitespace[trimmed - 1])
                        trimmed--;

                    int start, end;

                    if (p.getEnd() - p.getStart() == 0) {
                        if (node.getParent() != null) {
                            start = Math.min(node.getParent().getPosition().getEnd(),
                                    Math.max(node.getParent().getPosition().getStart(), p.getStart()));
                            end = start;
                        } else {
                            start = p.getStart();
                            end = start;
                        }
                    } else {
                        start = p.getStart();
                        end = Math.max(trimmed, start);
                    }

                    node.setPosition(new Position(start, end));

                    return false;
                }
            });
    }

    private void fixPositions(List<? extends Node> nodes) {
        for (Node node : nodes)
            node.accept(new ForwardingAstVisitor() {
                @Override
                public boolean visitNode(Node node) {
                    Position p = node.getPosition();
                    if (!p.isUnplaced()) {
                        node.setPosition(new Position(mapPosition(p.getStart()), mapPosition(p.getEnd())));
                    }
                    if (node instanceof Expression) {
                        List<Position> list = ((Expression) node).astParensPositions();
                        if (list != null) {
                            ListIterator<Position> li = list.listIterator();
                            while (li.hasNext()) {
                                Position parenPos = li.next();
                                if (!parenPos.isUnplaced()) {
                                    parenPos = new Position(mapPosition(parenPos.getStart()),
                                            mapPosition(parenPos.getEnd()));
                                    li.set(parenPos);
                                }
                            }
                        }
                    }
                    return false;
                }
            });
    }

    /**
     * Associates comments that are javadocs to the node they belong to, by checking if the node that immediately follows a javadoc node is a JavadocContainer.
     */
    private void associateJavadoc(List<Comment> comments, List<Node> nodes) {
        final TreeMap<Integer, Node> startPosMap = Maps.newTreeMap();
        for (Node node : nodes)
            node.accept(new ForwardingAstVisitor() {
                @Override
                public boolean visitNode(Node node) {
                    if (node.isGenerated())
                        return false;
                    int startPos = node.getPosition().getStart();
                    Node current = startPosMap.get(startPos);
                    if (current == null || !(current instanceof JavadocContainer)) {
                        startPosMap.put(startPos, node);
                    }

                    return false;
                }
            });

        for (Comment comment : comments) {
            if (!comment.isJavadoc())
                continue;
            Map<Integer, Node> tailMap = startPosMap.tailMap(comment.getPosition().getEnd());
            if (tailMap.isEmpty())
                continue;
            Node assoc = tailMap.values().iterator().next();
            if (!(assoc instanceof JavadocContainer))
                continue;
            JavadocContainer jc = (JavadocContainer) assoc;
            if (jc.rawJavadoc() != null) {
                if (jc.rawJavadoc().getPosition().getEnd() >= comment.getPosition().getEnd())
                    continue;
            }
            jc.rawJavadoc(comment);
        }
    }

    void registerComment(Context<Node> context, Comment c) {
        List<Comment> list = registeredComments.get(context);
        if (list == null) {
            list = Lists.newArrayList();
            registeredComments.put(context.getSubNodes().get(0), list);
        }
        list.add(c);
    }

    /**
     * Delves through the parboiled node tree to find comments.
     */
    private boolean gatherComments(org.parboiled.Node<Node> parsed) {
        boolean foundComments = false;
        for (org.parboiled.Node<Node> child : parsed.getChildren()) {
            foundComments |= gatherComments(child);
        }

        List<Comment> cmts = registeredComments.get(parsed);
        if (cmts != null)
            for (Comment c : cmts) {
                comments.add(c);
                return true;
            }

        return foundComments;
    }

    private void setPositionDelta(int position, int delta) {
        Integer i = positionDeltas.get(position);
        if (i == null)
            i = 0;
        positionDeltas.put(position, i + delta);
    }

    public List<Integer> getLineEndingsTable() {
        return lineEndings;
    }

    public long lineColumn(int index) {
        //Possible efficiency improvement: Store in a list the index into the line table with the first line that's over 500, 1000, 1500, ... chars.
        //Or just binary search.
        int oldIdx = 0;
        int line = 0;

        for (; line < lineEndings.size(); line++) {
            int pos = lineEndings.get(line);
            if (pos > index)
                break;
            oldIdx = pos;
        }
        return ((long) line << 32 | index - oldIdx);
    }

    /**
     * Maps a position in the {@code preprocessed} string to the equivalent character in the {@code rawInput}.
     * 
     * The difference is caused by decoding backslash-U unicode escapes, for example.
     */
    int mapPosition(int position) {
        int out = position;
        for (int delta : positionDeltas.headMap(position, true).values()) {
            out += delta;
        }
        return out;
    }

    private String preProcess() {
        preprocessed = rawInput;
        this.lineEndings = calculateLineEndings();
        applyBackslashU();
        //      applyBraceMatching();
        return preprocessed;
    }

    /**
     * @see <a href="http://java.sun.com/docs/books/jls/third_edition/html/lexical.html#3.3">JLS section 3.3</a>
     */
    private void applyBackslashU() {
        StringBuilder buffer = new StringBuilder();
        StringBuilder out = new StringBuilder();

        int state = 0;
        int idx = 0;
        for (char c : preprocessed.toCharArray()) {
            idx++;
            switch (state) {
            case 0: //normal mode. Anything that isn't a backslash is not interesting.
                if (c != '\\') {
                    out.append(c);
                    break;
                }

                state = 1;
                break;
            case 1: //Last character read is an (uneven amount of) backslash.
                if (c != 'u') {
                    out.append('\\');
                    out.append(c);
                    state = 0;
                } else {
                    buffer.setLength(0);
                    buffer.append("\\u");
                    state = 2;
                }
                break;
            //TODO add a test for more backslash-U stuff.
            default:
                //Gobbling hex digits. state-2 is our current position. We want 4.
                buffer.append(c);
                if (c == 'u') {
                    //JLS Puzzler: backslash-u-u-u-u-u-u-u-u-u-4hexdigits means the same thing as just 1 u.
                    //So, we just keep going as if nothing changed.
                } else if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
                    state++;
                    if (state == 6) {
                        //We've got our 4 hex digits.
                        out.append((char) Integer.parseInt(buffer.substring(buffer.length() - 4), 0x10));
                        int delta = buffer.length() - 1; //Buffer goes away but 1 character appears in its place.
                        setPositionDelta(idx - delta, delta);
                        buffer.setLength(0);
                        //We don't have to check if this char is a backslash and set state to 1; JLS says backslash-u is not recursively applied.
                        state = 0;
                    }
                } else {
                    //Invalid unicode escape.
                    problems.add(new ParseProblem(new Position(idx - buffer.length(), idx),
                            "Invalid backslash-u escape: \\u is supposed to be followed by 4 hex digits."));
                    out.append(buffer.toString());
                    state = 0;
                }
                break;
            }
        }

        preprocessed = out.toString();
    }
}