org.eclipse.jdt.internal.formatter.DefaultCodeFormatter.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.jdt.internal.formatter.DefaultCodeFormatter.java

Source

/*******************************************************************************
 * Copyright (c) 2000, 2019 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Jesper Steen Moller - Contributions for
 *                        bug 404146 - [1.7][compiler] nested try-catch-finally-blocks leads to unrunnable Java byte code
 *     Harry Terkelsen (het@google.com) - Bug 449262 - Allow the use of third-party Java formatters
 *     Mateusz Matela <mateusz.matela@gmail.com> - [formatter] Formatter does not format Java code correctly, especially when max line width is set - https://bugs.eclipse.org/303519
 *     Mateusz Matela <mateusz.matela@gmail.com> - [formatter] follow up bug for comments - https://bugs.eclipse.org/458208
 *     Lars Vogel <Lars.Vogel@vogella.com> - Contributions for
 *                       Bug 473178
 *******************************************************************************/
package org.eclipse.jdt.internal.formatter;

import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameCOMMENT_BLOCK;
import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameCOMMENT_JAVADOC;
import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameCOMMENT_LINE;
import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameEOF;
import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameNotAToken;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IModuleDescription;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.core.compiler.InvalidInputException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.Comment;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Javadoc;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.formatter.CodeFormatter;
import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.lookup.TypeConstants;
import org.eclipse.jdt.internal.compiler.parser.Scanner;
import org.eclipse.jdt.internal.compiler.util.Util;
import org.eclipse.jdt.internal.core.JavaProject;
import org.eclipse.jdt.internal.core.SourceModule;
import org.eclipse.jdt.internal.formatter.linewrap.CommentWrapExecutor;
import org.eclipse.jdt.internal.formatter.linewrap.WrapPreparator;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.TextEdit;

public class DefaultCodeFormatter extends CodeFormatter {

    /**
     * Debug trace
     */
    public static boolean DEBUG = false;

    private static final int K_COMMENTS_MASK = K_SINGLE_LINE_COMMENT | K_MULTI_LINE_COMMENT | K_JAVA_DOC;

    // Mask for code formatter kinds
    private static final int K_MASK = K_UNKNOWN | K_EXPRESSION | K_STATEMENTS | K_CLASS_BODY_DECLARATIONS
            | K_COMPILATION_UNIT | K_MODULE_INFO | K_COMMENTS_MASK;

    private static final Map<Integer, Integer> FORMAT_TO_PARSER_KIND = new HashMap<>();
    static {
        FORMAT_TO_PARSER_KIND.put(K_COMPILATION_UNIT, ASTParser.K_COMPILATION_UNIT);
        FORMAT_TO_PARSER_KIND.put(K_MODULE_INFO, ASTParser.K_COMPILATION_UNIT);
        FORMAT_TO_PARSER_KIND.put(K_CLASS_BODY_DECLARATIONS, ASTParser.K_CLASS_BODY_DECLARATIONS);
        FORMAT_TO_PARSER_KIND.put(K_STATEMENTS, ASTParser.K_STATEMENTS);
        FORMAT_TO_PARSER_KIND.put(K_EXPRESSION, ASTParser.K_EXPRESSION);
    }

    private DefaultCodeFormatterOptions originalOptions;
    private DefaultCodeFormatterOptions workingOptions;

    private Object oldCommentFormatOption;
    private String sourceLevel;
    public boolean previewEnabled;

    private String sourceString;
    char[] sourceArray;
    private List<IRegion> formatRegions;

    private ASTNode astRoot;
    private List<Token> tokens = new ArrayList<>();
    private TokenManager tokenManager;

    public DefaultCodeFormatter() {
        this(new DefaultCodeFormatterOptions(DefaultCodeFormatterConstants.getJavaConventionsSettings()), null);
    }

    public DefaultCodeFormatter(DefaultCodeFormatterOptions options) {
        this(options, null);
    }

    public DefaultCodeFormatter(Map<String, String> options) {
        this(null, options);
    }

    public DefaultCodeFormatter(DefaultCodeFormatterOptions defaultCodeFormatterOptions,
            Map<String, String> options) {
        initOptions(defaultCodeFormatterOptions, options);
    }

    private void initOptions(DefaultCodeFormatterOptions defaultCodeFormatterOptions, Map<String, String> options) {
        if (options != null) {
            this.originalOptions = new DefaultCodeFormatterOptions(options);
            this.workingOptions = new DefaultCodeFormatterOptions(options);
            this.oldCommentFormatOption = getOldCommentFormatOption(options);
            String compilerSource = options.get(CompilerOptions.OPTION_Source);
            this.sourceLevel = compilerSource != null ? compilerSource : CompilerOptions.VERSION_13;
            this.previewEnabled = JavaCore.ENABLED
                    .equals(options.get(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES));
        } else {
            Map<String, String> settings = DefaultCodeFormatterConstants.getJavaConventionsSettings();
            this.originalOptions = new DefaultCodeFormatterOptions(settings);
            this.workingOptions = new DefaultCodeFormatterOptions(settings);
            this.oldCommentFormatOption = DefaultCodeFormatterConstants.TRUE;
            this.sourceLevel = CompilerOptions.VERSION_13;
        }
        if (defaultCodeFormatterOptions != null) {
            this.originalOptions.set(defaultCodeFormatterOptions.getMap());
            this.workingOptions.set(defaultCodeFormatterOptions.getMap());
        }
    }

    @Deprecated
    private Object getOldCommentFormatOption(Map<String, String> options) {
        return options.get(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT);
    }

    @Override
    public String createIndentationString(final int indentationLevel) {
        if (indentationLevel < 0) {
            throw new IllegalArgumentException();
        }

        StringBuilder sb = new StringBuilder();
        int indent = indentationLevel * this.originalOptions.indentation_size;
        TextEditsBuilder.appendIndentationString(sb, this.originalOptions.tab_char, this.originalOptions.tab_size,
                indent, 0);
        return sb.toString();
    }

    /**
     * @see org.eclipse.jdt.core.formatter.CodeFormatter#format(int, java.lang.String, int, int, int, java.lang.String)
     */
    @Override
    public TextEdit format(int kind, String source, int offset, int length, int indentationLevel,
            String lineSeparator) {
        return format(kind, source, new IRegion[] { new Region(offset, length) }, indentationLevel, lineSeparator);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TextEdit format(int kind, String source, IRegion[] regions, int indentationLevel, String lineSeparator) {
        if (!regionsSatisfiesPreconditions(regions, source.length())) {
            throw new IllegalArgumentException();
        }
        this.formatRegions = Arrays.asList(regions);

        updateWorkingOptions(indentationLevel, lineSeparator, kind);

        if ((kind & K_COMMENTS_MASK) != 0)
            return formatComments(source, kind & K_COMMENTS_MASK);

        if (prepareFormattedCode(source, kind) == null)
            return this.tokens.isEmpty() ? new MultiTextEdit() : null;

        MultiTextEdit result = new MultiTextEdit();
        TextEditsBuilder resultBuilder = new TextEditsBuilder(this.sourceString, this.formatRegions,
                this.tokenManager, this.workingOptions);
        this.tokenManager.traverse(0, resultBuilder);
        for (TextEdit edit : resultBuilder.getEdits()) {
            result.addChild(edit);
        }
        return result;
    }

    private boolean init(String source, int kind) {

        // this is convenient for debugging (see Token.toString())
        // Token.source = source;

        this.sourceString = source;
        this.sourceArray = source.toCharArray();
        this.tokens.clear();
        this.tokenManager = new TokenManager(this.tokens, source, this.workingOptions);

        tokenizeSource(kind);
        return !this.tokens.isEmpty();
    }

    List<Token> prepareFormattedCode(String source) {
        this.formatRegions = Arrays.asList(new Region(0, source.length()));
        return prepareFormattedCode(source, CodeFormatter.K_UNKNOWN);
    }

    private List<Token> prepareFormattedCode(String source, int kind) {
        if (!init(source, kind))
            return null;

        this.astRoot = parseSourceCode(kind);
        if (this.astRoot == null)
            return null;

        if (kind != CodeFormatter.K_UNKNOWN)
            findHeader();

        prepareSpaces();
        prepareLineBreaks();
        prepareComments();
        prepareWraps(kind);

        return this.tokens;
    }

    private void findHeader() {
        if (this.astRoot instanceof CompilationUnit) {
            CompilationUnit unit = (CompilationUnit) this.astRoot;
            List<TypeDeclaration> types = unit.types();
            ASTNode firstElement = types.isEmpty() ? unit.getPackage() : types.get(0);
            if (firstElement != null) {
                int headerEndIndex = this.tokenManager.firstIndexIn(firstElement, -1);
                this.tokenManager.setHeaderEndIndex(headerEndIndex);
            }
        }
    }

    private TextEdit formatComments(String source, int kind) {
        MultiTextEdit result = new MultiTextEdit();
        if (!init(source, kind))
            return result;

        CommentsPreparator commentsPreparator = new CommentsPreparator(this.tokenManager, this.workingOptions,
                this.sourceLevel);
        CommentWrapExecutor commentWrapper = new CommentWrapExecutor(this.tokenManager, this.workingOptions);
        switch (kind) {
        case K_JAVA_DOC:
            for (Token token : this.tokens) {
                if (token.tokenType == TokenNameCOMMENT_JAVADOC) {
                    CompilationUnit cu = (CompilationUnit) parseSourceCode(ASTParser.K_COMPILATION_UNIT);
                    Javadoc javadoc = (Javadoc) cu.getCommentList().get(0);
                    javadoc.accept(commentsPreparator);
                    int startPosition = this.tokenManager.findSourcePositionInLine(token.originalStart);
                    commentWrapper.wrapMultiLineComment(token, startPosition, false, false);
                }
            }
            break;
        case K_MULTI_LINE_COMMENT:
            for (int i = 0; i < this.tokens.size(); i++) {
                Token token = this.tokens.get(i);
                if (token.tokenType == TokenNameCOMMENT_BLOCK) {
                    commentsPreparator.handleBlockComment(i);
                    int startPosition = this.tokenManager.findSourcePositionInLine(token.originalStart);
                    commentWrapper.wrapMultiLineComment(token, startPosition, false, false);
                }
            }
            break;
        case K_SINGLE_LINE_COMMENT:
            for (int i = 0; i < this.tokens.size(); i++) {
                Token token = this.tokens.get(i);
                if (token.tokenType == TokenNameCOMMENT_LINE) {
                    commentsPreparator.handleLineComment(i);
                    if (i >= this.tokens.size() || this.tokens.get(i) != token) {
                        // current token has been removed and merged with previous one
                        i--;
                        token = this.tokens.get(i);
                    }
                    int startPosition = this.tokenManager.findSourcePositionInLine(token.originalStart);
                    commentWrapper.wrapLineComment(token, startPosition);
                }
            }
            break;
        default:
            throw new AssertionError(String.valueOf(kind));
        }

        applyFormatOff();

        TextEditsBuilder resultBuilder = new TextEditsBuilder(source, this.formatRegions, this.tokenManager,
                this.workingOptions);
        resultBuilder.setAlignChar(DefaultCodeFormatterOptions.SPACE);
        for (Token token : this.tokens) {
            List<Token> structure = token.getInternalStructure();
            if (token.isComment() && structure != null && !structure.isEmpty())
                resultBuilder.processComment(token);
        }

        for (TextEdit edit : resultBuilder.getEdits()) {
            result.addChild(edit);
        }
        return result;
    }

    private ASTNode parseSourceCode(int kind) {
        kind = kind & K_MASK;
        if (kind != K_UNKNOWN) {
            ASTNode astNode = createParser(kind).createAST(null);
            if (kind == K_COMPILATION_UNIT || kind == K_MODULE_INFO)
                return astNode;
            return hasErrors(astNode) ? null : astNode;
        }

        int[] kindsToTry = { K_COMPILATION_UNIT, K_EXPRESSION, K_CLASS_BODY_DECLARATIONS, K_STATEMENTS,
                K_MODULE_INFO };
        for (int kindToTry : kindsToTry) {
            ASTNode astNode = createParser(kindToTry).createAST(null);
            if (!hasErrors(astNode)) {
                if (kindToTry == K_MODULE_INFO)
                    tokenizeSource(kindToTry); // run scanner again to get module specific tokens
                return astNode;
            }
        }
        return null;
    }

    private ASTParser createParser(int kind) {
        ASTParser parser = ASTParser.newParser(AST.JLS13);

        if (kind == K_MODULE_INFO) {
            parser.setSource(createDummyModuleInfoCompilationUnit());
        } else {
            parser.setSource(this.sourceArray);
        }
        parser.setKind(FORMAT_TO_PARSER_KIND.get(kind));

        Map<String, String> parserOptions = JavaCore.getOptions();
        parserOptions.put(CompilerOptions.OPTION_Source, this.sourceLevel);
        parserOptions.put(CompilerOptions.OPTION_DocCommentSupport, CompilerOptions.ENABLED);
        parserOptions.put(CompilerOptions.OPTION_EnablePreviews, CompilerOptions.ENABLED); //TODO
        parserOptions.put(CompilerOptions.OPTION_ReportPreviewFeatures, CompilerOptions.IGNORE);
        parser.setCompilerOptions(parserOptions);
        return parser;
    }

    private ICompilationUnit createDummyModuleInfoCompilationUnit() {
        IJavaProject dummyProject = new JavaProject() {
            @Override
            public Map<String, String> getOptions(boolean inheritJavaCoreOptions) {
                return new HashMap<>();
            }

            @Override
            public IModuleDescription getModuleDescription() throws JavaModelException {
                return new SourceModule(this, ""); //$NON-NLS-1$
            }
        };
        return new org.eclipse.jdt.internal.core.CompilationUnit(null, TypeConstants.MODULE_INFO_FILE_NAME_STRING,
                null) {
            @Override
            public char[] getContents() {
                return DefaultCodeFormatter.this.sourceArray;
            }

            @Override
            public IJavaProject getJavaProject() {
                return dummyProject;
            }
        };
    }

    private boolean hasErrors(ASTNode astNode) {
        CompilationUnit root = (CompilationUnit) astNode.getRoot();
        for (IProblem problem : root.getProblems()) {
            if (problem.isError())
                return true;
        }
        return false;
    }

    private void tokenizeSource(int kind) {
        this.tokens.clear();
        Scanner scanner = new Scanner(true, false, false/* nls */,
                CompilerOptions.versionToJdkLevel(this.sourceLevel), null/* taskTags */, null/* taskPriorities */,
                false/* taskCaseSensitive */, this.previewEnabled);
        scanner.setSource(this.sourceArray);
        scanner.fakeInModule = (kind & K_MODULE_INFO) != 0;
        while (true) {
            try {
                int tokenType = scanner.getNextToken();
                if (tokenType == TokenNameEOF)
                    break;
                Token token = Token.fromCurrent(scanner, tokenType);
                this.tokens.add(token);
            } catch (InvalidInputException e) {
                Token token = Token.fromCurrent(scanner, TokenNameNotAToken);
                this.tokens.add(token);
            }
        }
    }

    private void prepareSpaces() {
        SpacePreparator spacePreparator = new SpacePreparator(this.tokenManager, this.workingOptions);
        this.astRoot.accept(spacePreparator);
        spacePreparator.finishUp();
    }

    private void prepareLineBreaks() {
        LineBreaksPreparator breaksPreparator = new LineBreaksPreparator(this.tokenManager, this.workingOptions);
        this.astRoot.accept(breaksPreparator);
        breaksPreparator.finishUp();
        this.astRoot.accept(new OneLineEnforcer(this.tokenManager, this.workingOptions));
    }

    private void prepareComments() {
        CommentsPreparator commentsPreparator = new CommentsPreparator(this.tokenManager, this.workingOptions,
                this.sourceLevel);
        List<Comment> comments = ((CompilationUnit) this.astRoot.getRoot()).getCommentList();
        for (Comment comment : comments) {
            comment.accept(commentsPreparator);
        }
        commentsPreparator.finishUp();
    }

    private void prepareWraps(int kind) {
        WrapPreparator wrapPreparator = new WrapPreparator(this.tokenManager, this.workingOptions, kind);
        this.astRoot.accept(wrapPreparator);
        applyFormatOff();
        wrapPreparator.finishUp(this.astRoot, this.formatRegions);
    }

    private void applyFormatOff() {
        for (Token[] offPair : this.tokenManager.getDisableFormatTokenPairs()) {
            final int offStart = offPair[0].originalStart;
            final int offEnd = offPair[1].originalEnd;

            offPair[0].setWrapPolicy(null);
            offPair[0].setIndent(
                    Math.min(offPair[0].getIndent(), this.tokenManager.findSourcePositionInLine(offStart)));

            final List<IRegion> result = new ArrayList<>();
            for (IRegion region : this.formatRegions) {
                final int start = region.getOffset(), end = region.getOffset() + region.getLength() - 1;
                if (offEnd < start || end < offStart) {
                    result.add(region);
                } else if (offStart <= start && end <= offEnd) {
                    // whole region off
                } else {
                    if (start < offStart)
                        result.add(new Region(start, offStart - start));
                    if (offEnd < end)
                        result.add(new Region(offEnd + 1, end - offEnd));
                }
            }
            this.formatRegions = result;
        }
    }

    /**
     * True if
     * <li>1. All regions are within maxLength
     * <li>2. regions are sorted
     * <li>3. regions are not overlapping
     */
    private boolean regionsSatisfiesPreconditions(IRegion[] regions, int maxLength) {
        int regionsLength = regions == null ? 0 : regions.length;
        if (regionsLength == 0) {
            return false;
        }

        IRegion first = regions[0];
        if (first.getOffset() < 0 || first.getLength() < 0 || first.getOffset() + first.getLength() > maxLength) {
            return false;
        }

        int lastOffset = first.getOffset() + first.getLength() - 1;
        for (int i = 1; i < regionsLength; i++) {
            IRegion current = regions[i];
            if (lastOffset > current.getOffset()) {
                return false;
            }

            if (current.getOffset() < 0 || current.getLength() < 0
                    || current.getOffset() + current.getLength() > maxLength) {
                return false;
            }

            lastOffset = current.getOffset() + current.getLength() - 1;
        }

        return true;
    }

    private void updateWorkingOptions(int indentationLevel, String lineSeparator, int kind) {
        this.workingOptions.line_separator = lineSeparator != null ? lineSeparator
                : this.originalOptions.line_separator;
        if (this.workingOptions.line_separator == null)
            this.workingOptions.line_separator = Util.LINE_SEPARATOR;

        this.workingOptions.initial_indentation_level = indentationLevel;

        this.workingOptions.comment_format_javadoc_comment = this.originalOptions.comment_format_javadoc_comment
                && canFormatComment(kind, K_JAVA_DOC);
        this.workingOptions.comment_format_block_comment = this.originalOptions.comment_format_block_comment
                && canFormatComment(kind, K_MULTI_LINE_COMMENT);
        this.workingOptions.comment_format_line_comment = this.originalOptions.comment_format_line_comment
                && canFormatComment(kind, K_SINGLE_LINE_COMMENT);
    }

    private boolean canFormatComment(int kind, int commentKind) {
        if ((kind & F_INCLUDE_COMMENTS) != 0)
            return true;
        if (DefaultCodeFormatterConstants.FALSE.equals(this.oldCommentFormatOption))
            return false;
        if ((kind & K_MASK) == commentKind)
            return true;
        if (kind == K_UNKNOWN && DefaultCodeFormatterConstants.TRUE.equals(this.oldCommentFormatOption))
            return true;
        return false;
    }

    @Override
    public void setOptions(Map<String, String> options) {
        initOptions(null, options);
    }
}