All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.smithy.syntax.SortUseStatements Maven / Gradle / Ivy

/*
 * Copyright Amazon.com, Inc. or its affiliates. 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.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 software.amazon.smithy.syntax;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import software.amazon.smithy.model.loader.IdlToken;

/**
 * Sorts use statements in models alphabetically, case-sensitively.
 */
final class SortUseStatements implements Function {
    // One of the complications of this transformation is that comments that might document a use statement are
    // captured in the BR production that trails a use statement. So moving a use statement needs to move any trailing
    // comments from its previous sibling to the next previous sibling it's moved after. To make this even more
    // complicated, comments on the same line as a statement should stay with the statement rather than moved.
    @Override
    public TokenTree apply(TokenTree tree) {
        TreeCursor useSection = Objects.requireNonNull(tree.zipper().getFirstChild(TreeType.SHAPE_SECTION))
                .getFirstChild(TreeType.USE_SECTION);

        // All trees should have a USE_SECTION. If the section is empty, then no use statements need to be sorted.
        // Note that if it isn't empty, then there is a guaranteed NAMESPACE_STATEMENT too.
        if (useSection == null || useSection.getTree().isEmpty()) {
            return tree;
        }

        UseDataModel dataModel = new UseDataModel(tree.zipper(), useSection);

        // Remove the original statements since they get sorted and re-added.
        useSection.getChildrenByType(TreeType.USE_STATEMENT)
                .forEach(s -> useSection.getTree().removeChild(s.getTree()));

        dataModel.addFirstUse(useSection);
        dataModel.addSubsequentUse(useSection);

        return tree;
    }

    private static final class UseDataModel {
        List ids = new ArrayList<>();
        Map lineComments = new HashMap<>();
        Map> leadingComments = new HashMap<>();
        List endComments = new ArrayList<>();
        TreeCursor namespaceBrWs;

        UseDataModel(TreeCursor root, TreeCursor useSection) {
            List shapeIds = useSection.findChildrenByType(TreeType.ABSOLUTE_ROOT_SHAPE_ID);
            List comments = useSection.findChildrenByType(TreeType.COMMENT);

            int commentIndex = 0;
            for (TreeCursor id : shapeIds) {
                ids.add(id.getTree());
                while (commentIndex < comments.size()) {
                    TreeCursor comment = comments.get(commentIndex);
                    if (comment.getTree().getStartLine() == id.getTree().getStartLine()) {
                        lineComments.put(id.getTree(), comment);
                    } else if (comment.getTree().getStartLine() < id.getTree().getStartLine()) {
                        putLeadingComment(id.getTree(), comment);
                    } else {
                        break;
                    }
                    commentIndex++;
                }
            }

            // Any remaining comments are "sticky" since they are on the next section, not the use statement.
            while (commentIndex < comments.size()) {
                TreeCursor comment = comments.get(commentIndex);
                endComments.add(comment);
                commentIndex++;
            }

            findOrCreateNamespaceBrWs(root);

            ids.sort(Comparator.comparing(TokenTree::concatTokens));
        }

        private void findOrCreateNamespaceBrWs(TreeCursor root) {
            // The first use statement could have leading comments that are part of the previous section. If this
            // statement is moved, then the comments from the previous section need to be removed and moved into this
            // section, and potentially replaced with comments from USE_SECTION.
            TreeCursor br = root.findChildrenByType(TreeType.NAMESPACE_STATEMENT)
                    .get(0)
                    .findChildrenByType(TreeType.BR)
                    .get(0);

            namespaceBrWs = br.getFirstChild(TreeType.WS);

            if (namespaceBrWs != null) {
                List firstComments = namespaceBrWs.getChildrenByType(TreeType.COMMENT);
                putLeadingComments(ids.get(0), firstComments);
                // Remove these first comments since they get recreated when the use statements are sorted.
                firstComments.forEach(c -> namespaceBrWs.getTree().removeChild(c.getTree()));
            } else {
                // Create an empty WS node since we might need to add comments here later.
                br.getTree().appendChild(TokenTree.of(TreeType.WS));
                namespaceBrWs = br.getFirstChild(TreeType.WS);
            }
        }

        private void putLeadingComment(TokenTree id, TreeCursor comment) {
            putLeadingComments(id, Collections.singletonList(comment));
        }

        private void putLeadingComments(TokenTree id, List comments) {
            leadingComments.computeIfAbsent(id, i -> new ArrayList<>()).addAll(comments);
        }

        private List getTrailingComments(int index) {
            if (index == ids.size() - 1) {
                return endComments;
            } else {
                return leadingComments.computeIfAbsent(ids.get(index + 1), id -> new ArrayList<>());
            }
        }

        private void addFirstUse(TreeCursor useSection) {
            // Add the first statement. Swap out its leading comments from the preceding NAMESPACE_STATEMENT's BR WS.
            TokenTree id = ids.get(0);
            TokenTree useStatement = createUseStatement(id, lineComments.get(id), getTrailingComments(0));

            // Add any leading comments to the namespace BR WS so they aren't trailing comments.
            if (leadingComments.get(id) != null) {
                leadingComments.get(id).forEach(comment -> namespaceBrWs.getTree().appendChild(comment.getTree()));
            }

            useSection.getTree().appendChild(useStatement);
        }

        private void addSubsequentUse(TreeCursor useSection) {
            for (int i = 1; i < ids.size(); i++) {
                TokenTree id = ids.get(i);
                TokenTree useStatement = createUseStatement(id, lineComments.get(id), getTrailingComments(i));
                useSection.getTree().appendChild(useStatement);
            }
        }

        private TokenTree createUseStatement(TokenTree id, TreeCursor lineComment, List trailingComments) {
            TokenTree result = TokenTree.of(TreeType.USE_STATEMENT);

            CapturedToken useToken = CapturedToken.builder().token(IdlToken.IDENTIFIER).lexeme("use").build();
            result.appendChild(TokenTree.of(useToken));

            TokenTree sp = TokenTree.of(TreeType.SP);
            sp.appendChild(TokenTree.of(CapturedToken.builder().token(IdlToken.SPACE).lexeme(" ").build()));
            result.appendChild(sp);

            result.appendChild(id);

            TokenTree br = TokenTree.of(TreeType.BR);
            result.appendChild(br);

            if (lineComment != null) {
                br.appendChild(sp);
                br.appendChild(lineComment.getTree());
            } else {
                br.appendChild(TokenTree.of(CapturedToken.builder()
                        .token(IdlToken.NEWLINE)
                        .lexeme("\n")
                        // Set the start line to be the start line of the use statement
                        // so the formatter  knows it's a line comment.
                        .startLine(id.getStartLine())
                        .endLine(id.getStartLine() + 1)
                        .build()));
            }

            TokenTree ws = TokenTree.of(TreeType.WS);
            trailingComments.forEach(tc -> ws.appendChild(tc.getTree()));
            br.appendChild(ws);

            return result;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy