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

io.helidon.codegen.ModuleInfoSourceParser Maven / Gradle / Ivy

/*
 * Copyright (c) 2023, 2024 Oracle and/or its affiliates.
 *
 * 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 io.helidon.codegen;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import io.helidon.common.types.TypeName;

/**
 * Support for parsing module-info.java sources.
 */
public final class ModuleInfoSourceParser {
    private static final String PROVIDES = "provides";
    private static final String OPENS = "opens";
    private static final Pattern ANNOTATION = Pattern.compile("(@\\w+)(.*)");
    private static final Pattern OPENS_PATTERN = Pattern.compile("opens (.*?)(?:\\s+to\\s+(.*?))?");
    private static final Pattern EXPORTS_PATTERN = Pattern.compile("exports (.*?)(?:\\s+to\\s+(.*?))?");

    private final Map importAliases = new LinkedHashMap<>();
    private final List currentComments = new ArrayList<>();

    // state of the parser
    private State state = State.PRE_IMPORTS;
    // state of the parser outside of current comments
    private State outState = State.PRE_IMPORTS;
    // in progress string
    private String current;
    // opened brackets counter (used in annotation parsing)
    private int bracketsOpened;

    private ModuleInfoSourceParser() {
    }

    /**
     * Parse the module info from its input stream.
     *
     * @param inputStream input stream to parse
     * @return module info
     */
    public static ModuleInfo parse(InputStream inputStream) {
        return parse(new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)));
    }

    /**
     * Parse the module info from its source file.
     *
     * @param path path to parse
     * @return module info
     */
    public static ModuleInfo parse(Path path) {
        try (InputStream inputStream = Files.newInputStream(path)) {
            return parse(inputStream);
        } catch (IOException e) {
            throw new CodegenException("Failed to read module info from " + path.toAbsolutePath(), e);
        }

    }

    static ModuleInfo parse(BufferedReader reader) {
        ModuleInfoSourceParser parser = new ModuleInfoSourceParser();
        try {
            return parser.doParse(reader);
        } catch (IOException e) {
            throw new CodegenException("Failed to parse module info", e, ModuleInfoSourceParser.class);
        }
    }

    private ModuleInfo doParse(BufferedReader reader) throws IOException {
        ModuleInfo.Builder builder = ModuleInfo.builder();

        String line;
        while ((line = reader.readLine()) != null && state != State.DONE) {
            String inProgress = line;
            while (!inProgress.isEmpty() && state != State.DONE) {
                inProgress =
                        switch (state) {
                            case PRE_IMPORTS, POST_IMPORTS, MODULE_CONTENT, UNKNOWN -> nextState(builder, inProgress);
                            case M_COMMENTS -> mComments(inProgress);
                            case IMPORTS -> imports(inProgress);
                            case ANNOTATION -> annotation(builder, inProgress);
                            case MODULE_NAME -> moduleName(builder, inProgress);
                            case REQUIRES -> contentToSemi(builder, inProgress, this::parseRequires);
                            case EXPORTS -> contentToSemi(builder, inProgress, this::parseExports);
                            case USES -> contentToSemi(builder, inProgress, this::parseUses);
                            case PROVIDES -> contentToSemi(builder, inProgress, this::parseProvides);
                            case OPENS -> contentToSemi(builder, inProgress, this::parseOpens);
                            default -> throw new CodegenException("Unexpected parsing state: " + state);
                        };
            }
        }

        return builder.build();
    }

    private String annotation(ModuleInfo.Builder builder, String inProgress) {
        // we have processed @Something
        if (inProgress.isBlank()) {
            // next line
            return "";
        }
        if (inProgress.startsWith("(")) {
            bracketsOpened++;
        }
        int lastIndex = -1;
        while (bracketsOpened > 0) {
            int index = inProgress.indexOf(')', lastIndex + 1);
            if (index == -1) {
                break;
            }
            lastIndex = index;
            bracketsOpened--;
        }
        if (bracketsOpened > 0) {
            current = current + " " + inProgress;
            return "";
        }
        if (lastIndex >= 0) {
            return newState(State.POST_IMPORTS, inProgress.substring(lastIndex + 1));
        }
        return newState(State.POST_IMPORTS, inProgress);
    }

    private String moduleName(ModuleInfo.Builder builder, String inProgress) {
        int index = inProgress.indexOf('{');
        if (index > -1) {
            parseModuleName(builder, current + " " + inProgress.substring(0, index));

            return newState(State.MODULE_CONTENT, inProgress.substring(index + 1));
        }
        current = current + " " + inProgress;
        return "";
    }

    private void parseModuleName(ModuleInfo.Builder builder, String moduleNameString) {
        // such as `open module io.helidon.config`
        String[] split = moduleNameString.split("\\s+");

        boolean isOpen = false;
        String name = null;
        for (String nameElement : split) {
            if ("open".equals(nameElement)) {
                isOpen = true;
                continue;
            }
            if ("module".equals(nameElement)) {
                continue;
            }
            // last element is expected to be the module name
            name = nameElement;
        }
        if (name == null) {
            throw new CodegenException("Cannot discover module name from: " + moduleNameString);
        }
        builder.name(name);
        builder.isOpen(isOpen);
    }

    private String contentToSemi(ModuleInfo.Builder builder,
                                 String inProgress,
                                 BiConsumer parseMethod) {
        int index = inProgress.indexOf(';');
        if (index > -1) {
            parseMethod.accept(builder, current + "  " + inProgress.substring(0, index));

            return newState(State.MODULE_CONTENT, inProgress.substring(index + 1));
        }
        current = current + " " + inProgress;
        return "";
    }

    private String imports(String inProgress) {
        int index = inProgress.indexOf(';');
        if (index > -1) {
            parseImport(current + "  " + inProgress.substring(0, index));

            return newState(State.POST_IMPORTS, inProgress.substring(index + 1));
        }
        current = current + " " + inProgress;
        return "";
    }

    private String mComments(String inProgress) {
        int index = inProgress.indexOf("*/");
        if (index > 0) {
            state = outState;
            String comment = inProgress.substring(0, index).trim();
            if (!comment.isEmpty()) {
                currentComments.add(comment);
            }
            return inProgress.substring(index + 2);
        }
        currentComments.add(inProgress);
        return "";
    }

    private String nextState(ModuleInfo.Builder builder, String inProgress) {
        String trimmed = inProgress.trim();
        if (trimmed.isEmpty()) {
            return "";
        }
        // now we know we have something, let's handle it
        if (trimmed.startsWith("/*")) {
            // M_COMMENTS
            if (trimmed.startsWith("/**/")) {
                // empty comment
                return trimmed.substring(4);
            }

            int begin = 2;

            if (trimmed.startsWith("/**")) {
                // javadoc
                begin = 3;
            }

            int endOfComments = trimmed.indexOf("*/");
            if (endOfComments > 0) {
                // end on the same line
                String comment = trimmed.substring(begin, endOfComments);
                currentComments.add(comment);
                return trimmed.substring(endOfComments + 2);
            }
            String comment = trimmed.substring(begin);
            if (!comment.isEmpty()) {
                currentComments.add(comment);
            }
            outState = state;
            state = State.M_COMMENTS;
            return "";
        }
        if (trimmed.startsWith("//")) {
            currentComments.add(trimmed.substring(2));
            return "";
        }
        if (trimmed.startsWith("import") && (state == State.PRE_IMPORTS || state == State.POST_IMPORTS)) {
            if (state == State.PRE_IMPORTS) {
                // builder.headerComment(String.join("\n", currentComments));
                currentComments.clear();
            }

            int index = trimmed.indexOf(';');
            if (index > 0) {
                // single line import statement
                parseImport(trimmed.substring(0, index).trim());
                return newState(State.POST_IMPORTS, trimmed.substring(index + 1));
            }
            // beginning of multiline import
            return stateContinuation(State.IMPORTS, trimmed);
        }

        if (trimmed.startsWith("@")) {
            return analyzeAnnotation(builder, trimmed);
        }

        if (state == State.PRE_IMPORTS || state == State.POST_IMPORTS) {
            // whatever we have, it must be module declaration
            // we handle multiline comments, comments, imports, and annotations above this section
            if (!currentComments.isEmpty()) {
                //                builder.descriptionComment(String.join("\n", currentComments));
                currentComments.clear();
            }
            int index = trimmed.indexOf('{');
            if (index > 0) {
                parseModule(builder, trimmed.substring(0, index).trim());
                return trimmed.substring(index + 1);
            }
            return stateContinuation(State.MODULE_NAME, trimmed);
        }

        if (trimmed.startsWith("requires") && state == State.MODULE_CONTENT) {
            int index = trimmed.indexOf(';');
            if (index > 0) {
                parseRequires(builder, trimmed.substring(0, index).trim());
                return trimmed.substring(index + 1);
            }

            return stateContinuation(State.REQUIRES, trimmed);
        }
        if (trimmed.startsWith("exports") && state == State.MODULE_CONTENT) {
            int index = trimmed.indexOf(';');
            if (index > 0) {
                parseExports(builder, trimmed.substring(0, index).trim());
                return trimmed.substring(index + 1);
            }

            return stateContinuation(State.EXPORTS, trimmed);
        }
        if (trimmed.startsWith("uses") && state == State.MODULE_CONTENT) {
            int index = trimmed.indexOf(';');
            if (index > 0) {
                parseUses(builder, trimmed.substring(0, index).trim());
                return trimmed.substring(index + 1);
            }

            return stateContinuation(State.USES, trimmed);
        }
        if (trimmed.startsWith("provides") && state == State.MODULE_CONTENT) {
            int index = trimmed.indexOf(';');
            if (index > 0) {
                parseProvides(builder, trimmed.substring(0, index).trim());
                return trimmed.substring(index + 1);
            }

            return stateContinuation(State.PROVIDES, trimmed);
        }
        if (trimmed.startsWith("opens") && state == State.MODULE_CONTENT) {
            int index = trimmed.indexOf(';');
            if (index > 0) {
                parseOpens(builder, trimmed.substring(0, index).trim());
                return trimmed.substring(index + 1);
            }

            return stateContinuation(State.OPENS, trimmed);
        }

        if (trimmed.startsWith("}")) {
            // we are done
            return stateContinuation(State.DONE, "");
        }

        //        builder.addUnhandledLine(trimmed);
        return "";
    }

    /**
     * Set new current state, and use the string as the new current value.
     *
     * @param newState   new state
     * @param newCurrent value to continue with parsing
     * @return empty string
     */
    private String stateContinuation(State newState, String newCurrent) {
        state = newState;
        outState = newState;
        current = newCurrent;
        return "";
    }

    /**
     * Set new current state, and return the remaining line.
     *
     * @param newState   new state
     * @param inProgress remainder of the line after parsing current value
     * @return inProgress
     */
    private String newState(State newState, String inProgress) {
        state = newState;
        outState = newState;
        current = "";
        return inProgress;
    }

    private String analyzeAnnotation(ModuleInfo.Builder builder, String annotationString) {
        Matcher matcher = ANNOTATION.matcher(annotationString);
        if (matcher.matches()) {
            current = matcher.group(1);
            return newState(State.ANNOTATION, matcher.group(2));
        } else {
            throw new CodegenException("Invalid annotation in module-info.java: " + annotationString);
        }
    }

    private void parseOpens(ModuleInfo.Builder builder, String opensString) {
        // opens X to Y, Z

        Matcher m = OPENS_PATTERN.matcher(opensString);
        if (m.matches()) {
            String first = m.group(1);
            String second = m.group(2);
            if (second == null) {
                builder.putOpen(first, List.of());
            } else {
                builder.putOpen(first, Arrays.stream(second.split(","))
                        .map(String::trim)
                        .toList());
            }
        } else {
            String inProgress = opensString.substring(OPENS.length()).trim();
            int toIndex = inProgress.indexOf("to");
            if (toIndex < 0) {
                throw new CodegenException("Cannot parse opens in module-info.java: " + opensString);
            }

            String what = inProgress.substring(0, toIndex).trim();
            String to = inProgress.substring(toIndex + 2).trim();
            List toList = Arrays.stream(to.split(","))
                    .map(String::trim)
                    .toList();

            builder.putOpen(what, toList);
        }
    }

    private void parseProvides(ModuleInfo.Builder builder, String providesString) {
        // provides X with Y, Z

        String inProgress = providesString.substring(PROVIDES.length()).trim();
        int withIndex = inProgress.indexOf("with");
        if (withIndex < 0) {
            throw new CodegenException("Cannot parse provides in module-info.java: " + providesString);
        }

        String what = checkImports(inProgress.substring(0, withIndex).trim());
        String with = inProgress.substring(withIndex + 5).trim();
        List withList = Arrays.stream(with.split(","))
                .map(String::trim)
                .map(this::checkImports)
                .map(TypeName::create)
                .toList();

        builder.putProvide(TypeName.create(what), withList);
    }

    private void parseUses(ModuleInfo.Builder builder, String usesString) {
        String usedType = checkImports(usesString.substring(4).trim());

        builder.addUse(TypeName.create(usedType));
    }

    private String checkImports(String typeName) {
        TypeName imported = importAliases.get(typeName);
        return imported == null ? typeName : imported.fqName();
    }

    private void parseExports(ModuleInfo.Builder builder, String exportsString) {
        // either "exports package"
        Matcher m = EXPORTS_PATTERN.matcher(exportsString);
        if (m.matches()) {
            String first = m.group(1);
            String second = m.group(2);
            if (second == null) {
                builder.putExports(first, List.of());
            } else {
                builder.putExports(first, Arrays.stream(second.split(","))
                        .map(String::trim)
                        .toList());
            }
        } else {
            // or "exports package to module name (fallback if does not match)
            builder.putExports(exportsString.substring(7).trim(), List.of());
        }
    }

    private void parseRequires(ModuleInfo.Builder builder, String requiresString) {
        boolean isStatic = false;
        boolean isTransitive = false;
        String target = null;
        String[] split = requiresString.split("\\s+"); // split by one or more whitespaces

        for (String element : split) {
            if (element.equals("static")) {
                isStatic = true;
                continue;
            }
            if (element.equals("transitive")) {
                isTransitive = true;
                continue;
            }
            target = element;
        }

        if (target == null) {
            throw new CodegenException("Failed to parse module-info.java line " + requiresString);
        }

        // requires static/transitive something;
        builder.addRequire(new ModuleInfoRequires(target, isTransitive, isStatic));
    }

    private void parseModule(ModuleInfo.Builder builder, String moduleString) {
        // module some.name
        builder.name(moduleString.substring(6).trim());
        state = State.MODULE_CONTENT;
        outState = State.MODULE_CONTENT;
    }

    private void parseImport(String importStatement) {
        // expects import a.b.c

        String importString = importStatement.substring(6).trim();
        String importedType = importString.substring(importString.lastIndexOf('.') + 1);
        importAliases.put(importedType, TypeName.create(importString));
    }

    enum State {
        PRE_IMPORTS,
        POST_IMPORTS,
        M_COMMENTS,
        IMPORTS,
        ANNOTATION,
        MODULE_NAME,
        MODULE_CONTENT,
        REQUIRES,
        EXPORTS,
        USES,
        PROVIDES,
        OPENS,
        DONE,
        UNKNOWN
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy